一、类加载过程

- 类加载检查
- 当Java虚拟机(JVM)遇到
new关键字时,它会先检查要创建的对象类是否已经被加载、链接和初始化。如果尚未加载,JVM会通过类加载器(ClassLoader)加载对应类的.class文件。
- 当Java虚拟机(JVM)遇到
- 类加载
- 类加载包括三个子步骤:加载、连接、初始化。
- 加载:通过权限定类名,读取 class 文件内容为二进制流;二进制流转换成方法区(永久代或元数据区)的运行时 C++类字节码对象 Klass;最后再在堆上生成一个 Class 对象,用来间接获取元数据区的类定义信息,静态对象也保存在 Class 对象中。
- 连接 :
- **验证:**验证文件格式、字节码元数据、语法、符号引用;
- 准备:为类的静态变量分配内存,并赋予默认初始值(例如0或null),但不会执行任何实际的初始化代码。
- **解析:**将符号引用替换为直接引用。
- 初始化:类的静态变量赋予正确的初始值,执行静态块;可能涉及父类初始化。
- 类加载包括三个子步骤:加载、连接、初始化。
- 内存分配
- JVM为新创建的对象分配内存空间。对象内存主要包括对象头、实例数据以及可能的对齐填充。
- 对象头:存储对象自身的元数据如哈希码、锁状态标志、GC分代年龄等信息,以及指向其类元数据(Class对象)的指针。
- 实例数据:存储类中定义的字段的实际数据。
- 对齐填充:非必须,为了满足JVM对内存地址对其的要求而填充的额外空间。
- JVM为新创建的对象分配内存空间。对象内存主要包括对象头、实例数据以及可能的对齐填充。
- 初始化零值 :
- 分配内存后,JVM会对对象的所有字段(包括实例变量)分配默认的初始值。
- 显式初始化:接下来,如果有在字段声明时直接赋予的初始值(例如int a = 10;),这些值会在构造函数执行前被赋予相应的变量。
- **对象头 必要信息设置 **主要是对象头中类的源数据信息,哈希码 ,对象 GC 分代年龄等。
- 初始化
- 构造器初始化:调用类的构造方法(即构造器)进行初始化,执行构造器中的初始化代码,此时才会给实例变量赋予程序员指定的初始值。
- 如果类中有父类并且还没有被初始化,则先初始化父类。
- 对象构造完成 :
- 构造方法执行完毕后,对象就完全构造出来了,可以被程序正常使用。
二、对象内存分配方式
内存分配方式根据不同的收集器策略可分为两种,不同的收集器的堆内存规整程度不一致所以有两种分配策略。
指针碰撞 Bump The Pointer
在使用指针碰撞策略时,Java堆被假设为一个连续的内存空间,被分为已用和未用两部分,中间由一个指针作为分界线。当新对象需要内存时,JVM只需将指针向未用空间一侧移动与对象大小相等的距离即可。这种方式适用于使用标记-清除 或复制算法的垃圾收集器,因为这些算法能够整理出连续的内存空间。
空闲列表 Free List
如果Java堆中的内存不是连续的,或者已被使用的内存和未被使用的内存相互交错(这种情况通常发生在使用标记-整理或分代收集算法的垃圾收集器中),那么空闲列表策略更为适用。在这种情况下,JVM维护一个列表来记录堆中各个小块的可用内存空间。当新对象需要内存时,JVM会从列表中找到一个足够大的空闲块分配给对象,并更新列表。这种方法不需要连续的内存空间,但管理成本相对较高。
三、内存分配的安全问题
堆是线程间共享的一块儿区域,所以多个线程同时创建对象时都涉及对内存空间的申请和分配,那么内存分配就可能出现线程安全问题。依赖以下机制解决多线程安全问题,一种是** CAS 乐观锁机制**,一种是 TLAB 本地线程分配缓冲机制
**Thread Local Allocation Buffer (TLAB) 本地线程分配缓冲:**每个线程有一个属于自己的预分配内存空间,JVM 首先通过 CAS 为线程申请一块儿预分配内存。这样当某个线程需要申请新的内存空间时首先现在自己的 TLAB 上分配,能减少内存分配冲突。后续 TLAB 内存不足了才会 CAS 申请一块儿新的 LATB 或者直接在 Eden 区直接分配。
**Compare-and-Swap (CAS)**在某些JVM实现中,可能会使用CAS操作来实现无锁的线程安全内存分配。CAS是一种硬件级别的原子操作,允许线程在不加锁的情况下比较并交换内存中的值,从而减少锁带来的性能开销,并能有效防止数据竞争。
四、对象如何进入老年代
- 新生代:刚创建的对象默认进入新生代的 Eden 区
- 进入老年代的条件:四种情况
- 熬过了多次 minorGC ,每次 MinorGC 过后对象的年龄就会+1,存活超过 15 次之后就会进入老年代。该次数可通过参数控制
-XX:MaxTenuringThreshold - 动态年龄判断机制:MinorGC 后,如果 Survivor 区中的一批对象大雨了这块 Survivor 区的 50%就会将大于等于这批对象年龄最大值的所有对象直接进入老年代。
- 举例 S1 中有 年龄为 1 、2、3、4 的一批对象,其中 234 年龄的加起来超过 S1 的 50%,那么年龄大于等于 4 的对象就直接进入老年代了。
- Serial 和 ParNew 收集器,大对象直接进入老年代。例如大字符串和数组 可通过-XX:PertenureSizeThreshold 配置 默认为 1M
- MinorGC 后,存活的对象太多无法放入 Sruvivor 区域,会触发空间分配担保机制。将存活的对象移入老年代
- 熬过了多次 minorGC ,每次 MinorGC 过后对象的年龄就会+1,存活超过 15 次之后就会进入老年代。该次数可通过参数控制
分配担保机制(空间担保) Allocation Assurance Mechanism

什么是分配担保机制
在 JVM 中,空间分配担保机制(Space Allocation Guarantee Mechanism)是一种确保在进行垃圾收集时,有足够的空间来处理对象晋升 和分配 的策略。这种机制主要用于新生代垃圾收集(Minor GC)和老年代垃圾收集(Major GC 或 Full GC)之间的协调,以避免出现内存不足的情况。
:::success
用老年代的空间,来担保新生代的垃圾回收可以成功执行并腾出空间。会将新生代存活的对象转移到老年代中。保证新分配内存能直接成功。
- young 分区内存不足以创建新对象
:::
内存担保的原理
MinorGC前
- 第一步:判断老年代可用内存是否小于新时代对象全部对象大小,如果小于则继续判断,大于则可进行MainorGC
- 第二步:老年代小于存活对象,则判断老年代内存是否小于每次MinorGC后进入老年代的平均大小
- 小于平均大小,则进行FullGC,再判断是否能保存得下存活对象,放不下则OOM
- 大于平均大小,则进行MinorGC
MinorGC后
- 如果存活对象小于Survivor区,则直接进入Survivor区
- 如果存活对象大于Survivor区,但是小于老年代可用内存,则直接进入老年代
- 如果存活对象大于Survivor区,还大于老年代,则尝试进行一次FullGC ,FullGC后再次判断,如果放不下存活对象则会OOM
分配担保的配置
- **-XX:HandlePromotionFailure:**这个参数控制是否允许晋升失败。如果设置为 true,JVM 会在 Minor GC 时尝试晋升对象,即使老年代空间不足,也会尝试进行一次 Minor GC。如果失败,则触发 Full GC。这个参数在 Java 6 之后已经被默认取消使用。
- -XX:PretenureSizeThreshold:这个参数指定大对象直接在老年代分配的大小阈值。超过该阈值的对象直接分配到老年代,避免在新生代频繁复制。
- **-XX:MaxTenuringThreshold:**这个参数控制对象在新生代中经历多少次 GC 后晋升到老年代。较高的阈值可以减少对象晋升,但会增加新生代的 GC 频率。
- -XX:TargetSurvivorRatio:这个参数控制每次 Minor GC 后目标存活区(Survivor Space)的利用率。JVM 会根据这个参数调整对象晋升的阈值。
五、验证
大对象直接进入老年代
java
/**
* 测试:大对象直接进入到老年代
* -Xmx60m -Xms60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* -XX:PretenureSizeThreshold
*
*/
public class YoungOldArea {
public static void main(String[] args) {
byte[] buffer = new byte[1024*1024*20]; //20M
}
}
-XX:NewRatio=2 新生代与老年代比值
-XX:SurvivorRatio=8 新生代中,Eden与两个Survivor区域比值
-XX:+PrintGCDetails 打印详细GC日志
-XX:PretenureSizeThreshold 对象超过多大直接在老年代分配,默认值为0,不限制

对象内存分代晋升演示
java
/*
-Xmx600m -Xms600m -XX:+PrintGCDetails
*/
public class HeapInstance {
public static void main(String[] args) {
List<Picture> list = new ArrayList<>();
while (true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length){
this.pixels = new byte[length];
}
}


通过可视化插件可以看到
- Eden区满了之后,就会进行MinorGC,MinorGC时会将Survior放不下的对象存到old老年代
- 老年代也满了之后,发生了三次MinorGC,未释放出可用空间后,进行了三次FullGc最后抛出了OOM