文章

JVM 内存分配

对象的创建

1. 类加载

判断目标类有没有加载过,没加载过才会去加载

2. 分配内存

类加载过后,创建对象需要分配的内存大小就已确定,为对象分配内存相当于在堆中划出一个区域给即将创建的对象。

对象在内存中由对象头、实例数据、对齐填充这 3 部分构成,它们之和就是对象的大小:

可以通过 jol-core 库查看对象的大小(JOL, Java Object Layout):

默认使用 “指针碰撞(Bump the Pointer)” 的方式来划分内存,使用的内存放到一侧,空闲的内存在另一侧,中间使用一个指针作为分界点,那么分配内存就是把指针向空闲内存的那一侧移动一段与对象大小相等的距离。

同一时刻可能有多个线程在创建对象,JVM 默认使用 “本地线程分配缓冲(TLAB, Thread Local Allocation Buffer)” 机制来解决分配内存的并发问题,TLAB 把不同线程的内存分配动作隔离在不同的空间中进行,每个线程在堆中有一块预先分配的专属的内存用来创建对象(默认大小为Eden区空间的1%)

通过 -XX:+/-UseTLAB 参数来控制是否开启 TLAB,通过 -XX:TLABSize 来制定每个线程的 TLAB 大小

3. 初始化

将成员变量初始化为零值

4. 设置对象头

对象头由 Mark Word、Klass Pointer、Array Length 这 3 个部分构成:

  1. Mark Word 在 32 位系统中占 4 个字节,在 64 位系统中占 8 个字节

  2. Klass Pointer 指向方法区中的类元信息(如:可通过该指针去方法区找要执行的方法),在对象指针压缩生效时占 4 个字节,否则占 8 个字节

  3. Array Length 是有数组对象才存在

对象指针压缩:

JVM 从 jdk 1.6 update14 开始支持对象指针压缩,可以将 64 位的指针压缩成 32 位,它是默认开启的,通过 -XX:+/-UserCompressedOops 参数控制是否开启指针压缩(OOP,Ordinary Object Pointer),通过 -XX:+/-UserCompressedClassPointers 参数控制只压缩 Klass Pointer

堆内存小于 4G 时,32位地址就够了,因此不用开启指针压缩;堆内存大于 32G(35位) 时,指针压缩会失效

Mark Word可能的状态(64位HotSpot):


> 可参考源代码:https://github.com/openjdk/jdk/blob/master/src/hotspot/share/oops/markWord.hpp

5. 执行<init>方法

首先会调用父类的 <init> 方法,其次执行自身的 <init> 方法,包括:给成员变量赋值、执行代码块、执行构造方法

> 注意区分类加载时的 <cinit> 初始化方法

对象内存分配

对象栈上分配

创建新对象时,JVM 会优先尝试在栈上分配,分配在栈上的对象会随着栈帧出栈而销毁,可以减轻了垃圾回收的压力。

栈上分配依赖于对象逃逸分析(-XX:+/-DoEscapeAnalysis)和标量替换(-XX:+/-EliminateAllocations),JDK 7 之后默认开启。

对象逃逸分析是什么?

分析方法中创建的对象是否被外部方法所引用,如果一个对象没有被外部方法所引用,那么这个对象就没有逃逸出当前方法,可以尝试在栈上分配。

/**
 * 创建的 order 对象逃逸出了当前方法
 */
public Order createOrder() {
    Order order = new Order();
    order.setId(UUID.randomUUID().toString());
    order.setTime(Instant.now());
    return order;
}

/**
 * 创建的 order 对象没有逃逸出当前方法
 */
public void printOrder() {
    Order order = new Order();
    order.setId(UUID.randomUUID().toString());
    order.setTime(Instant.now());
    System.out.println(order);
}

标量替换又是什么?

标量是指不可再分解的量,如 int、long 这些基本数据类型。

通过逃逸分析判断对象没有逃逸,且对象可以进一步分解时,JVM 会将该对象成员变量拆分后在栈帧上分配,这样不会因为没有连续内存空间导致稍微大一些的对象无法在栈上分配。

对象堆中分配

分配在Eden

大多数情况下,对象分配在 Eden 区。

当 Eden 区空间不足时,将触发 Minor GC。

Eden 区和两个 Survivor 区默认大小比为 8:1:1(默认开启的 -XX:UseAdaptiveSizePolicy 参数会自动调整这个比例)

进入老年代

  1. 大对象

  2. 动态年龄判断

  3. 连续存活对象

  4. To Survivor 空间不足

老年代空间分配担保

JVM 将堆内存划分为年轻代和老年代,两块内存使用不同的垃圾回收算法,因此在 Minor GC 之前,JVM 会检查老年代空间是否足够,判断应该执行 Minor GC 还是 Full GC。

对象回收规则

引用类型

  1. 强引用:普通的变量引用,只要被GC Root对象直接或间接依赖就不会被回收,如 User user = new User() 中的 user 就是强引用

  2. 软引用:SoftReference<T>,一般不会回收,GC后内存不足以存放新对象时才会回收,适合用作可有可无的缓存

  3. 弱引用:很少用,GC会直接回收掉

  4. 虚引用:很少用,GC会直接回收掉

finalize() 方法

对象被回收前,JVM 会调用对象的 finalize() 方法。

GC会回收方法区的无用类

什么是无用类?

  1. 类的所有实例已被回收

  2. 加载该类的 ClassLoader 已被回收(一般是自定义类加载器,如JspClassLoader)

  3. 类的 Class 对象没有被任何地方引用

License:  CC BY 4.0