likes
comments
collection
share

那么大个对象的垃圾回收有什么不同

作者站长头像
站长
· 阅读数 9

前言

说到JDK1.8的垃圾回收,想必大家都可以在脑海中浮现如下一张流程图。

那么大个对象的垃圾回收有什么不同

上述流程图示意了JDK1.8在默认情况下也就是Parallel Scavenge + Serial Old(PS MarkSweep) 垃圾收集器组合下的一个大致垃圾回收过程。但是如果是大对象的话,实际垃圾回收可能和上面的流程还稍有区别,具体的区别在于如下两点。

  1. ParNew或者SerialOld垃圾收集器下,使用-XX:PretenureSizeThreshold参数指定一个阈值,大小大于指定阈值的对象将直接进入老年代;
  2. 如果大对象是一个在新生代分配失败且不包含任何对象引用的数组,则直接进入老年代。

上述第一点想必大家是背烂了的,但是第二点应该稍有陌生。针对第二点,在《Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine》一文中有如下一段话进行阐述。

 If an allocation fails in the young generation and the object is a large array that does not contain any references to objects, it can be allocated directly into the old generation. In some select instances, this strategy was intended to avoid a collection of the young generation by allocating from the old generation.

那么本文就针对上述的这种不包含任何对象引用的数组的垃圾回收进行一个探究。

正文

首先提供如下两个方法来创建本文讨论的不包含任何对象引用的数组这种大对象。

private final List<Byte[]> bytesList = new ArrayList<>();  
  
public void addMemory() {  
    Byte[] bytes = new Byte[1024 * 1024 * 50];  
    bytesList.add(bytes);  
}  
public void addMemoryAllowGc() {  
    Byte[] bytes = new Byte[1024 * 1024 * 50];  
}  

区别在于第一个方法创建的大对象无法被GC,而第二个方法创建的大对象可以被GC,此外两个方法每次创建的对象占用内存大约为200MB

把程序运行起来时,初始状态下内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

此时eden区的内存使用近乎是满的状态,而old区近乎没有被使用。现在调用addMemoryAllowGc() 方法,创建可以被GC的200MB的大对象,内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

可见直接在old区完成了大对象创建。继续调用addMemoryAllowGc() 方法,创建可以被GC的200MB的大对象,内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

继续调用addMemoryAllowGc() 方法,创建可以被GC的200MB的大对象,内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

此时eden区和old区都无法再分配出200MB的内存空间,现在再调用一次addMemoryAllowGc() 方法,创建可以被GC的200MB的大对象,内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

我们在eden区和old区剩余内存都不足200MB的情况下,又想要创建一个200MB的大对象,首先会尝试在eden区创建,发现eden区剩余内存不足以创建,然后又会尝试在old区创建,发现old区剩余内存也不足以创建,此时就会执行一次Young GC(上图中Young GC次数加了1),Young GC完毕后,会再尝试在eden区创建对象,因为Young GC完毕后eden区已经有足够的内存空间了,所以我们本次创建的大对象就创建在了eden区。现在继续调用addMemory() 方法创建不可以被GC的200MB的大对象,内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

还是同样的道理,eden区和old区都没有足够的空间来创建对象,所以先执行一次Young GC(上图中Young GC次数加了1),然后再在eden区创建对象,但是注意,最终eden区创建出来的200MB的大对象是无法被GC的(对应的Byte数组对象存在强引用)。现在再调用一次addMemoryAllowGc() 方法,创建可以被GC的200MB的大对象,内存使用率和GC次数如下所示。

那么大个对象的垃圾回收有什么不同

可见最终Young GCOld GC次数均加1,这是因为一开始eden区和old区都没有足够的空间来创建对象,所以先执行了一次Young GC(上图中Young GC次数加了1),但是eden区此时是有一个被强引用的大对象占据了约200MB的内存空间,这个大对象在Young GC后会存活下来,但此时survivor区无法容纳这个大对象,所以这个大对象直接进入old区,可old区此时也无法容纳这个大对象,最终就触发了Old GC,之后old区就有了足够的内存空间容纳这个大对象,再之后新创建的对象就在eden区完成创建。这个过程也可以看作是发生了一次Full GC,因为Young GCOld GC均发生了一次。

总结

现在对不包含任何对象引用的大数组这样的对象的GC的一个具体流程进行总结。

  1. 新对象优先在eden区创建;
  2. 新对象如果过大,导致eden区分配内存失败,则新对象直接在old区创建;
  3. 如果old区也无法容纳新对象,则触发Young GC
  4. Young GC时存活下来的对象会进入survivor区,如果survivor区无法容纳,则存活下来的对象直接进入old区,如果old区无法容纳,则触发Old GC

注:本文如果有概念错误的地方,欢迎在下方评论区留言讨论,万分感激!