07、JVM 调优实战 - 对象在JVM内存中如何分配与流转?

1. 两种对象

代码里创建出来的对象,一般有如下两种:

  • 一种是短期存活的,分配在 Java 堆内存之后,迅速使用完就会被垃圾回收;
  • 另外一种是长期存活的,需要一直生存在 Java 堆内存里,让程序后续不停的去使用

前者的对象,是在 Java 堆内存的新生代里;而后者的对象,是在 Java 堆内存的老年代里。

2. 大部分正常对象都优先在新生代分配内存

问题:对象肾情况下短期存活?

要知道,大部分正常对象,都优先在新生代分配内存的。

 

按上一章所说,类静态变量 "fetcher" 引用的 “ReplicaFetcher” 对象,是会长期存活在内存里的。但是,它在开始通过 ”new ReplicaFetcher()“ 代码来实例化一个对象时,也是先分配在新生代里的。

包括在“loadReplicasFromDisk()” 方法中创建 “ReplicaManager”实例对象,也是先分配在新生代里的。

如下图:

 

3. 什么情况下会触发新生代的垃圾回收?

在“loadReplicasFromDisk()” 方法执行完毕后,该方法的栈帧出栈,会导致没有任何局部变量引用那个 “ReplicaManager” 实例对象。

 

在没有局部变量引用实例对象的情况下,不会立即发生垃圾回收。因为垃圾回收是有触发的条件的。

触发垃圾回收的场景:

当程序创建了N多对象,然后导致 Java堆内存里囤积了大量的对象。在新生代的内存空间几乎被全部对象占满的情况下,如果要给新的对象分配内存,此时发现新生代里内存空间不够用。

而此时新生代中有部分对象的局部变量没有人引用。就会触发一次新生代内存空间的垃圾回收,也就是 “Minor GC”,也叫 “Young GC” ,这是常识把新生代里那些没有人引用的垃圾对象,都给回收掉。

在触发一次垃圾回收,就会把所有垃圾对象给干掉,从而腾出大量的内存空间。如下图:

 

4. 长期存活的对象会躲过多次垃圾回收

如“ReplicaFetcher” 实例对象这种长期被 “Kafka” 类的静态变量“fetcher” 引用的长期存活对象。在新生代进行垃圾回收时,会一直存活,而不被回收。

JVM中有一条规定,如果一个实例对象在新生代中,成功的在 15 次垃圾回收之后,还是没被回收掉,就说明它已经15岁了。(每经历一次垃圾回收增加1岁)

如示例中的 “ReplicaFetcher” 对象,它在新生代如果成功躲过 15次垃圾回收,就会被认为是会长期存活在内存里的对象。

从而被转移到 Java 堆内存的老年代中去。如下图:

 

5. 老年代会垃圾回收吗?

老年代里的对象也是会被垃圾回收的。因为老年代的对象也有可能随着代码的运行,不再被引用到,就需要被垃圾回收。

6. 关于新生代和老年代的对象分配,还存在哪些机制

新生代和老年代,在对象分配方面,还有很多的复杂机制,比如:

  • 新生代垃圾回收之后,因为存活对象太多,导致大量对象直接进入老年代;
  • 特别大的超大对象直接不经过新生代就进入老年代;
  • 动态对象年龄判断机制;
  • 空间担保机制

小小的总结:

  • 理解对象优先分配在新生代;
  • 新生代如果对象满了,会触发 Minor GC 回收掉没有人引用的垃圾对象;
  • 如果有对象躲过了十多次垃圾回收,就会放入老年代里;
  • 如果老年代也满了,那么也会触发垃圾回收,把老年代里没人引用的垃圾对象清理掉。

7.思考题

每个线程都有 Java 虚拟机栈,里面也有方法的局部变量等数据,这个 Java虚拟机栈需要进行垃圾回收吗?为什么?

答:JVM里垃圾回收针对的是新生代,老年代,还有方法区(永久代),不会针对方法的栈帧。方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉了。