一、JVM对象创建
在Java程序中,对象的创建是通过类加载器来实现的。JVM在创建对象之前,会先加载该对象所属的类,然后再创建对象。
1. 类加载器
Java中的类加载器是用来加载类的,它将类的字节码文件加载到内存中,并生成对应的Class对象。Java中的类加载器有三种:
1)Bootstrap ClassLoader:它是JVM的内置类加载器,用来加载Java的核心类库,如java.lang和java.util等。
2)Extension ClassLoader:它用来加载Java的扩展类库,如javax等。
3)System ClassLoader:它用来加载应用程序的类,如自定义的类等。
2. 类加载过程
类加载的过程可以分为三个步骤:加载、链接和初始化。
1)加载:类加载器首先会从文件系统、网络或其他来源中加载类的字节码文件。在加载过程中,类加载器会生成对应的Class对象,并将其存放在JVM的方法区中。
2)链接:链接分为三个阶段:验证、准备和解析。
验证:验证阶段用来确保类的字节码文件符合JVM规范,并且不包含安全漏洞等问题。
准备:准备阶段用来为类的静态变量分配内存,并设置默认值。
解析:解析阶段用来将类的符号引用转换为直接引用。
3)初始化:初始化阶段用来执行类的静态初始化代码块,并初始化静态变量。在初始化阶段,JVM会按照静态变量的定义顺序执行静态初始化代码块。
3. JVM对象创建时类加载的过程
在JVM对象创建时,类加载器会先加载该对象所属的类,然后再创建对象。具体的过程如下:
1)检查类是否已经被加载,如果没有,就先加载该类。
2)在堆内存中分配一块空间来存放对象。
3)对对象进行初始化,包括设置对象的成员变量和执行构造函数。
4)返回对象引用。
二、内存分配机制
1. JVM内存划分如下图
程序计数器
程序计数器是JVM中的一块较小的内存区域,它用于记录当前线程所执行的字节码的位置。由于Java是一种解释性语言,每条指令都需要翻译成机器码后才能执行,因此程序计数器就是记录正在执行的字节码指令的位置的指针。
Java虚拟机栈
Java虚拟机栈是线程私有的,用于存储方法调用过程中的局部变量、方法参数和返回值等数据。每个方法在执行的时候都会在栈上创建一个栈帧,栈帧中包含了该方法的局部变量表、操作数栈、方法返回地址等信息。随着方法的执行结束,栈帧也会随之出栈销毁。
本地方法栈
本地方法栈与Java虚拟机栈类似,不同的是本地方法栈是为虚拟机使用到的本地(Native)方法服务的。
Java堆
Java堆是Java虚拟机管理的最大一块内存区域,被所有线程共享。Java堆是用来存储对象实例和数组对象的内存区域,由于垃圾回收器会在Java堆中进行垃圾回收,因此Java堆被划分为年轻代和老年代两部分。
年轻代
年轻代是Java堆中的一部分,它又被分为三个部分:Eden空间、Survivor空间From和Survivor空间To。每个对象在创建的时候都会被分配到Eden空间,当Eden空间满了之后,虚拟机会对Eden空间中的存活对象进行一次垃圾回收,将存活的对象复制到Survivor空间From中,并将Eden空间中的所有对象清空。当Survivor空间From也满了之后,虚拟机会对Survivor空间From和Survivor空间To中的存活对象进行一次垃圾回收,将存活的对象复制到Survivor空间To中,并将Survivor空间From中的所有对象清空。当对象在Survivor空间中存活了一定的次数之后,虚拟机会将其晋升到老年代中。
老年代
老年代是Java堆中的一部分,用于存储长时间存活的对象。在进行垃圾回收时,虚拟机会对老年代中的存活对象进行标记-清除、标记-整理等垃圾回收算法。
方法区
方法区也是被所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK8之前,方法区被称为永久代(PermGen),但是永久代容易出现内存溢出的问题,因此在JDK8之后被移除,被元空间(Metaspace)取代。
2. JVM内存分配
在JVM中,对象的内存分配是由Java堆中的内存分配器负责的。Java堆中的内存分配器主要分为两种:Serial收集器和并行收集器。
Serial收集器
Serial收集器是一种单线程的垃圾回收器,它会暂停所有Java线程来进行垃圾回收。Serial收集器主要适用于单核CPU和小内存的环境,它的优点是简单高效,但是不适合大规模的应用场景。
并行收集器
并行收集器是一种多线程的垃圾回收器,它可以利用多核CPU来并发地执行垃圾回收操作,从而减少垃圾回收的暂停时间。并行收集器适用于大规模应用和高并发环境,但是它也有一些缺点,例如垃圾回收期间会消耗大量的CPU资源,可能会导致应用的响应时间变长。
3. 对象的内存分配过程
在JVM中,当需要创建一个新的对象时,虚拟机会首先在Eden空间中查找是否有足够的空间来存放该对象。如果Eden空间中的剩余空间不够,虚拟机会触发一次Minor GC来对Eden空间进行垃圾回收,从而为该对象腾出空间。如果Eden空间中的剩余空间足够,虚拟机会在Eden空间中为该对象分配内存。
当对象在Eden空间中存活了一定的时间后,虚拟机会将其移动到Survivor空间中。当Survivor空间也满了之后,虚拟机会触发一次Minor GC来对Survivor空间进行垃圾回收,从而为对象腾出空间。如果Survivor空间也不足以存放该对象,虚拟机会将该对象直接分配到老年代中。
在老年代中分配内存时,虚拟机会检查老年代中是否有足够的连续空间来存放该对象。如果老年代中的空间足够,虚拟机会为该对象分配内存。如果老年代中的空间不足,虚拟机会触发一次Full GC来对整个堆空间进行垃圾回收,从而为该对象腾出空间。
三、内存分配方式
JVM的内存分配方式有两种:栈内存和堆内存。栈内存是用来存放基本数据类型和对象引用的,而堆内存是用来存放对象的。具体的内存分配方式如下:
1)栈内存:栈内存是一种后进先出的数据结构,用来存放基本数据类型和对象引用。当程序调用一个方法时,JVM会为该方法创建一个栈帧,用来存放方法的参数、局部变量和返回值等信息。当方法执行完毕时,JVM会将该栈帧出栈,释放栈内存。
2)堆内存:堆内存是用来存放对象的。当程序创建对象时,JVM会在堆内存中分配一块空间来存放对象。堆内存的大小可以通过JVM参数进行调整。
对于堆内存还存在两种内存分配方式:指针碰撞和空闲列表
1. 指针碰撞
指针碰撞是一种内存分配方式,它适用于堆内存中的连续空间。在这种情况下,JVM会将堆内存划分为两个区域:一部分用来存放已经分配的对象,另一部分则是未分配的空间。JVM会使用一个指针来记录已经分配的空间的末尾位置,当程序需要创建一个对象时,JVM会检查未分配的空间是否有足够的空间来存放该对象,如果有,JVM会将指针移动到未分配空间的起始位置,并将该对象存放在指针所指向的位置。如果没有足够的空间,JVM会触发一次垃圾回收操作,释放一些已经不再使用的空间,然后再次尝试分配空间。
2. 空闲列表
空闲列表是一种内存分配方式,它适用于堆内存中的非连续空间。在这种情况下,JVM会将堆内存划分为多个大小不同的块,每个块都有一个头部信息,用来记录该块是否已经被分配和块的大小等信息。JVM会使用一个链表来记录所有未被分配的块,当程序需要创建一个对象时,JVM会遍历空闲列表,查找是否有足够大小的块来存放该对象,如果有,JVM会将该块分配给该对象,并将该块从空闲列表中移除。如果没有足够的空间,JVM会触发一次垃圾回收操作,释放一些已经不再使用的空间,然后再次尝试分配空间。
栈内存用来存放基本数据类型和对象引用,堆内存用来存放对象。
指针碰撞适用于堆内存中的连续空间,空闲列表适用于堆内存中的非连续空间。
四、对象头
在JVM中,每个对象都有一个对象头,它是对象在内存中的元数据信息,包括对象的类型、锁状态、GC信息等。
1. JVM对象头的结构
JVM对象头的结构包含两部分:Mark Word和Class Pointer。其中,Mark Word是对象头的核心部分,它占用了8个字节,Class Pointer占用了4个字节。下面分别介绍这两部分的作用和结构。
-
Mark Word
Mark Word是对象头的核心部分,它包含了对象的类型、锁状态、GC信息等。Mark Word的结构如下:
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
hashcode | age | biased_lock | lock_state | identity_hashcode | unused | CMS_mark | CMS_remark | CMS_bits | reserved | thread | epoch | unused | unused | forward |
其中,各字段的含义如下:
hashcode:对象的哈希码,用于快速定位对象在哈希表中的位置。
age:对象的年龄,用于标记对象是否经过了一次Minor GC。
biased_lock:偏向锁标记,用于标记对象是否处于偏向锁状态。
lock_state:锁状态,用于标记对象的锁状态,包括无锁、轻量级锁、重量级锁等。
identity_hashcode:对象的标识哈希码,用于在JVM中识别对象。
unused:未使用字段。
CMS_mark:CMS标记位,用于标记对象是否被CMS GC标记。
CMS_remark:CMS重新标记位,用于标记对象是否被CMS GC重新标记。
CMS_bits:CMS位图,用于标记对象在CMS GC中的状态。
reserved:保留字段。
thread:线程ID,用于标记当前持有锁的线程ID。
epoch:时间戳,用于标记偏向锁的时间戳。
forward:对象是否被移动的标记位。
Mark Word的结构可以根据JVM的版本和配置不同而有所不同,但是大致的结构是相似的。
2. Class Pointer
Class Pointer是对象头的另一部分,它占用了4个字节,用于指向对象的类元数据信息。在JVM中,每个类都有一个Class对象,它包含了类的基本信息,如类名、父类、接口、方法等。当对象被创建时,JVM会将Class对象的指针存储在对象头的Class Pointer中,以便在运行时快速访问对象的类信息。
JVM对象头的作用
JVM对象头是对象在内存中的元数据信息,它包含了对象的类型、锁状态、GC信息等。JVM通过对象头来管理对象的生命周期和状态,包括对象的创建、访问、锁定、垃圾回收等。下面分别介绍JVM对象头的几个重要作用。
- 类型标记
JVM对象头中的Mark Word包含了对象的类型信息,用于标记对象的类型。在JVM中,每个对象都有一个类型,它决定了对象的行为和属性。JVM通过对象头中的类型标记来判断对象的类型,以便在运行时调用对象的方法和属性。
- 锁状态标记
JVM对象头中的Mark Word包含了对象的锁状态信息,用于标记对象的锁状态。在JVM中,对象可以被多个线程同时访问,为了保证线程安全,JVM通过对象头中的锁状态标记来管理对象的锁状态。在JVM中,锁状态包括无锁、轻量级锁、重量级锁等,不同的锁状态对应不同的锁实现方式。
- GC标记
JVM对象头中的Mark Word包含了对象的GC信息,用于标记对象的GC状态。在JVM中,对象的内存分配和回收是由垃圾回收器来管理的,JVM通过对象头中的GC标记来管理对象的GC状态。在JVM中,GC标记包括标记位、位图等,用于标记对象是否需要被垃圾回收器回收。
- Class Pointer
JVM对象头中的Class Pointer用于指向对象的类元数据信息,以便在运行时快速访问对象的类信息。在JVM中,每个类都有一个Class对象,它包含了类的基本信息,如类名、父类、接口、方法等。JVM通过对象头中的Class Pointer来访问对象的类信息,以便在运行时调用对象的方法和属性。
JVM对象头是对象在内存中的元数据信息,它包含了对象的类型、锁状态、GC信息等。JVM通过对象头来管理对象的生命周期和状态,包括对象的创建、访问、锁定、垃圾回收等。
五、执行init方法
在Java中,每个类都有一个默认的构造函数,用于初始化对象的成员变量。但是,有时候我们需要在对象创建时执行一些特殊的操作,比如初始化静态变量、读取配置文件等。这时候就需要使用init方法来完成这些操作。
1. 什么是init方法
init方法是一种特殊的Java方法,它的作用是在对象创建后进行初始化工作。init方法的命名规则是以“<init>”开头,后面跟随着一些参数,用于初始化对象的状态。init方法的定义在Java代码中不可见,它是由Java编译器自动产生的。在Java中,每个类都可以定义一个init方法,它必须满足以下条件:
init方法必须是非静态方法。
init方法的名称可以是任意的,但是通常为init或者initialize。
init方法的返回值类型必须为void。
init方法不能被重载。
init方法的访问权限可以是public、protected或者默认访问权限,但是不能是private。
在Java中,init方法通常用于初始化静态变量、读取配置文件、建立数据库连接等。在Spring框架中,init方法还被用于执行Bean的初始化操作。
2. JVM执行init方法的过程
当JVM执行new关键字创建一个对象时,会分配一块内存空间用于存储对象的实例变量,并初始化对象头信息。在初始化完对象头信息后,JVM会自动调用该对象的init方法进行进一步的初始化工作。
init方法的调用过程是由Java虚拟机完成的,其具体过程如下:
在堆上分配对象的内存空间,包括对象头和实例变量。
对象头信息初始化完成后,JVM会将对象的引用作为参数传递给init方法。
JVM会查找对象所属类的init方法。如果该类没有定义init方法,则会向上查找其父类的init方法,直到Object类为止。
JVM在找到init方法后,会将对象引用作为参数传递给init方法,并调用该方法进行对象的进一步初始化工作。
当init方法执行完毕后,JVM将该对象的引用返回给程序,使得程序可以通过该引用来访问对象。
需要注意的是,JVM只会调用一个对象的init方法一次。如果一个对象被多次创建,它的init方法也只会被调用一次。另外,JVM会确保在调用init方法之前,对象的实例变量已经被正确初始化。
JVM在执行init方法时是单线程的,即同一时间只有一个线程在执行init方法。这是因为对象的初始化操作通常是非常耗时的,如果允许多个线程同时执行init方法,就会导致竞争条件和线程安全问题。
总结:
JVM的对象创建、内存分配机制、内存分配方式、设置对象头、执行init方法是Java程序中非常重要的一部分。了解这些机制可以帮助我们更好地理解Java程序的运行机制,从而更好地编写高质量的Java程序。在实际开发中,我们应该注意对象的创建和内存分配,避免对象的过多创建和内存泄漏等问题,从而提高程序的性能和稳定性。