JVM-04-对象在内存中的流转

239 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

对象在内存中的流转

上一部分说明了一个对象在内存中的生成,这一部分将围绕对象在生成后如何在内存中流转,对象进入Eden后会发生什么,什么时候会进入老年代,如何判断对象是否存活等问题。本部分非常重要,因为很多JVM方面的优化都与这一部分有关。

对象在Eden区分配

所有新创建的对象都会先分配在Eden区中,如果Eden区的空间不足,将会进行一次Minor GC。Eden区和两个Survivor区的默认比例为8:1:1,由于对象基本上是朝生夕灭的,因此大部分对象都会被回收。在做Minor GC后,eden区和其中一个Survivor区会进行垃圾回收,再把剩余的存活对象复制到另外的一个Survivor区中。

如果说Survivor空间不足的话,部分对象会提前转移到老年代,接下来就我们就说说怎样会流转到老年代中。

对象流转到老年代

在一般的印象里,对象流转到老年代通常是由于分代年龄到达设定值,但是除了这种情况还有其他两种比较特殊的情况。

1.长期存活的对象进入老年代

这种是最“正常”的流转到老年代的情况,通过设置 -XX:MaxTenuringThreshold 参数可以指定晋升至老年代的阈值,默认情况下是15岁(也就是经历了15次Minor GC),CMS垃圾回收器默认6岁。为了做到这一点,JVM还需要给对象分配年龄的计数器。

2.大对象直接进入老年代

大对象是指需要大块连续内存空间的对象,比如数组,字符串等等。进入老年代是为了避免在Minor GC后进行复制导致效率较低。可以通过参数 -XX:PretenureSizeThreshold 设置大对象的阈值,但是这个参数只能在Serial和ParNew两个收集器使用。

3.对象动态年龄判断

这种情况就是之前提到的Survivor空间不足会把一部分对象提前移到老年代。如果放对象的Survivor区中一批对象大于Survivor内存的50%(该比例可以通过参数 -TargetSurvivorRatio 设定),那么此时大于等于这批对象的分代年龄最大值的对象就可以直接进入老年代。举例就是年龄1+年龄2+年龄n的对象超过了设定的值,那么年龄n以上的所有对象都直接进入老年代。这种情况通常发生在Minor GC后发生。

垃圾回收的时机

一般来说,在eden区满时会触发Minor GC,在老年代满时会触发Full GC。但是还有一种比较特别的情况会提前触发Full GC,那就是老年代空间分配担保机制。

老年代空间分配担保机制

该机制通过参数 XX-HandlePromotionFailure 设置,在JDK1.8后默认开启。如果说在Minor GC前,老年代的剩余空间大于新生代的所有对象空间,那么就正常地做Minor GC,这是正常的一步。但是如果说老年代的剩余空间更小的话,如果不配置这个参数的话会直接进行一次Full GC,但是如果配置了就会多分析一步,看看现在老年代的剩余空间是不是要比之前新生代每一次Minor GC后进入老年代的对象平均大小要大,如果老年代剩余空间更大,则会先进行一次Minor GC,否则的话还是会进行Full GC。

也就是说,老年代空间分配担保,其实就是在触发Full GC之前再进行了一次判断。

怎样会被判定为垃圾?

上文讲到了垃圾回收,那么到底什么样的对象会被判定为垃圾呢?

引用计数法

这种方法是最简单的一种判断垃圾对象的方法,如果一个地方引用这个对象,那么引用计数器就加一,如果引用失效就减一。一个引用计数为0的对象,不可能再被引用,那么该对象就是垃圾对象。

但是大多数的垃圾回收器都并没有使用这种方法,原因是因为这种方法会发生循环引用的问题。如果对象A引用对象B,对象B引用对象A,其他任何地方都没有用到他们,但是他们的引用计数器并不是零,就无法回收,显然这并不是很理想的结果。

可达性分析算法

为改进这种方法,我们引入了“GC Root”的概念。

GC Root简单来讲就是绝对不会被回收的对象,比如说虚拟机栈中引用的对象,持用同步锁的对象,类静态常量引用的对象以及JVM内部引用的一些对象等等。我们从GC Root出发,向下搜索引用的对象,搜索到的对象就不是垃圾对象。

引用的分类

既然提到了引用,其实引用也是分成很多类的,我们日常开发时用到的引用被称为强引用,还有另外的几种引用:

软引用:这种引用是第二强的引用,当GC完成后仍然没有充足的空间,会把这一部分引用回收,这种引用适合用来做高速缓存

pubilc static SoftReference<User> user = new SoftReference<>(new User());

弱引用:这种引用会在下一次垃圾回收时立即被回收

pubilc static WeakReference<User> user = new WeakReference<>(new User());

虚引用:它是最弱的一种引用关系,几乎不用,只有当所有引用被回收或自身变得不可访问后,虚引用指向的对象才会回收

finalize()对象最后的救赎

并不是说一个对象在判定为垃圾对象就一定会被回收,它还有最后的一个机会,那就是finalize方法。如果一个对象重写了该方法,并且与引用链上的对象产生了联系,那么就可以逃脱被清除的命运。

但是该方法一般不用,因为有可能会造成严重的内存泄漏问题。

垃圾回收并不仅限于堆中

方法区其实也可能发生回收,我们知道方法区是存储类信息的,如果要在方法区内回收内存,那么就必须判断这个类是一个无用的类。必须同时满足以下三个情况,才是无用的类:

1.该类所有的实例都被回收了

2.加载该类的ClassLoader被回收了

3.没有任何一个地方引用了该类的Class对象

由于条件比较苛刻,所以通常不会在方法区中发生垃圾回收的。

总结

本部分介绍了一个对象在内存中的流转,包括新生代,老年代之间的流转,以及在什么情况下会被认定为垃圾,并且还介绍了垃圾回收的时机等。本部分其实非常重要,因为JVM调优其实主要就是围绕着如何减少Full GC的发生,因为Full GC非常地花时间,如果了解了对象是如何流转的,如何尽量让对象被及时回收,我相信调优问题也就引刃而解了。