1、对象创建的主要流程
1.1、类加载检查
虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,必须先执行相应的类加载过程。
new指令对应到语言层面:new关键词、对象克隆、对象序列化等。
1.2、 加载类
作用:负责将.class 文件中的字节码加载到内存中,并将它们转换为JVM可以理解的内部数据结构。
主要过程:包括加载、验证、准备、解析和初始化五个阶段。
1.3、分配内存
对象创建需要在堆中分配一块内存。JVM根据对象的大小(由对象的类型信息确定)从堆中分配相应大小的空间。
分配方式取决于堆是否是连续的:
指针碰撞(Bump the Pointer):如果堆内存是连续的,JVM使用一个指针,指向堆内存的空闲区域的开始位置,新对象创建时,将指针向后移动对象大小的距离。
空闲列表(Free List):如果堆内存不连续,JVM会维护一个空闲列表,记录哪些内存块可以被分配,选择合适大小的内存分配给新对象。
为了保证多线程情况下的内存分配安全,JVM通过TLAB(Thread Local Allocation Buffer)机制将一部分堆内存分配给各个线程。每个线程在自己的TLAB中分配对象,避免锁开销。
1.4、初始化零值
将分配到的内存空间都初始化零值(不包括对象头),如果使用TLAB,这个工作过程可以提前到TLAB分配时进行。
目的: 保证对象的实例字段在Java代码中可以不赋初始值就能直接使用,程序能访问到这些字段的数据类型所对应的零值。
1.5、设置对象头
HotSpot虚拟机的对象头包括两部分信息:
第一部分用于存储对象自身的运行时数据,比如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对象头由3部分组成:
(1) mark word标记字段,32位 4字节, 64位8字节
(2) Klass Pointer类型指针,开启压缩 4字节, 关闭压缩 8字节
(3) 数组长度 (只有数组对象才有), 4字节
1.6、执行init方法
执行init方法,即对象按程序员的意愿进行初始化。对应到语言层面,就是是为属性赋值 (和上面的赋零值不同) 和执行构造方法。
可能会存在对象半初始化问题,对象在其构造方法还未完全执行完时被其他线程访问的情况就是对象半初始化。
产生对象半初始化的原因
(1) 构造方法内部泄露this引用;
(2) 双重检查锁定模式中的失效。
避免对象半初始化的策略
(1) 避免在构造法中泄露this引用,即不要在构造方法中将this传递给其他对象或发布到其他线程。
(2) 使用final字段。
(3) 使用静态初始化块或枚举来实现单例模式。
2、内存分配机制
JVM的内存分配机制主要集中在堆内存中,用于管理对象的创建、分配、回收等过程。JVM的内存分配机制在不同版本中有所优化,但整体分配逻辑主要围绕 堆区 和 方法区,并通过垃圾收集机制来管理对象生命周期。
2.1、堆内存分代模型
JVM的堆内存通常分为新生代和老年代,便于不同生命周期的对象使用不同的内存管理策略:
新生代(Young Generation):
新生代主要存放生命周期较短的对象,通常包括刚创建的对象。新生代进一步划分为Eden区和两个Survivor区(S0和S1)。
Eden区:对象最初在Eden区分配,空间用满时会触发Minor GC。
Survivor区:对象在Eden区存活并经过几次GC后,会被复制到Survivor区,经过一定次数的GC后还存活的对象会移动到老年代。
老年代(Old Generation):
老年代用于存放生命周期较长的对象,一般是经过多次GC仍然存活的对象。老年代的GC通常称为Major GC或Full GC,成本较高,触发频率低。
2.2、内存分配策略
内存分配策略决定了对象在不同区域的分配规则和管理方式:
优先分配到Eden区:
大多数情况下,对象优先分配在Eden区。如果Eden区满了,则触发Minor GC,将存活的对象移到Survivor区。
大对象直接进入老年代:
某些大对象(如大数组或大量占用内存的对象)会直接分配到老年代,以避免在新生代频繁复制带来的性能开销。
具体大小阈值可通过参数-XX:PretenureSizeThreshold
进行设置。
长期存活对象进入老年代:
JVM会为每个对象设置一个年龄计数器,每经过一次Minor GC,计数器加1。当计数达到一定阈值(默认15,可通过-XX:MaxTenuringThreshold
设置)时,进入老年代。
空间分配担保机制:
Minor GC前,JVM会检查老年代的剩余空间是否足够容纳新生代中的所有对象。如果不够,直接触发Full GC;如果足够,则继续进行Minor GC。
2.3、TLAB分配机制(Thread Local Allocation Buffer)
为减少多线程情况下内存分配的竞争,JVM为每个线程分配了一个私有缓冲区(TLAB),方便每个线程可以在自己的TLAB中分配小对象。
TLAB的分配:每个线程都会在Eden区内申请一块TLAB区域。当对象大小小于TLAB时,将直接在TLAB中分配,避免锁竞争。
TLAB的回收:当TLAB空间不足时,会重新申请新TLAB或直接在Eden区分配空间。
2.4、方法区(包括元空间)
方法区:用于存储类信息、常量、静态变量、即时编译器(JIT)编译后的代码等。JDK 8之前,方法区也称为永久代(PermGen),JDK 8之后移除了永久代,改为元空间(Metaspace),并且不再占用堆内存,而是使用本地内存。
元空间(Metaspace):JDK 8后,将类元数据存储在元空间中,元空间大小默认不固定,可以根据需求动态扩展大小,可通过-XX:MetaspaceSize
来调整初始大小。
2.5、垃圾回收(Garbage Collection)
新生代GC(Minor GC):当新生代空间不足时触发,清理无用对象,存活对象转移到Survivor或老年代。
老年代GC(Major GC 或 Full GC):当老年代空间不足时触发,清理老年代对象。相比Minor GC,Major GC的时间较长,影响较大,因此会尽量减少触发频率。
3、我的公众号
敬请关注我的公众号:大象只为你,持续更新技术知识......
原文始发于微信公众号(大象只为你):JVM对象创建与内存分配机制
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论