JVM-垃圾回收及内存分配机制

289 阅读10分钟

作为Javaer,经常会见到或面试时被问到垃圾回收(GC)的相关内容,那到底有哪些内容需要关注呢?

Java与C++之间有一堵由内存动态分配和垃圾回收技术所围成的围墙,墙外的人想进去,墙里面的人却想出来

垃圾回收(Garbage Collection,GC):顾名思义,就是回收内存垃圾所占用的空间,防止内存泄漏,有效的使用内存空间,对内存中长时间未使用或者已经死亡的对象进行整理和清除

什么是垃圾?哪些内存空间需要被回收

在堆中存放着Java程序中几乎所有的对象实例,对其进行垃圾回收的第一件事就是确定哪些对象需要“存活”,哪些对象已经“死亡”

判断对象的“存亡”,常用的有两种算法

1、引用计数法

在对象被创建时,JVM会给其分配一个引用计数器,当有一个地方引用时,计数器就加一;当不再引用时,计数器就减一。任何时刻引用计数器值为0的对象就是不可能再被使用的对象。引用计数法的原理简单,判定效率也很高,但是在主流的Java虚拟机中并没有选择引用计数算法来管理内存,主要原因,是因为引用计数算法无法解决对象之间相互引用的问题

如下代码,我们让两个实例对象相互引用,然后将其置空,通过查看GC日志,观察两个对象是否被当作垃圾回收,内存占用是否有变

如何查看GC日志

启动参数中设置 -XX:+PrintGCDetails

图片

public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过 */ private byte[] bigSize = new byte[5 * _1MB];

public static void testGC(){
    ReferenceCountingGC referenceA = new ReferenceCountingGC();
    ReferenceCountingGC referenceB = new ReferenceCountingGC();

    referenceA.instance = referenceB;
    referenceB.instance = referenceA;

    referenceA = null;
    referenceB = null;

    System.gc();
}

public static void main(String[] args) {
    testGC();
}

}

未启用GC的内存占用情况

image.png

启用GC后的内存占用情况

image.png

从上可知,System.gc()确实执行了垃圾回收过程,内存占用情况也从15444K->650K,两个对象的大小不止650K,新生代肯定放不下,可以断定,确实被回收掉了。这也证明了当前JVM使用的垃圾回收算法不是引用计数法

2、可达性分析算法

可达性分析算法的思路是从一系列被称为“GC Roots”的根对象为起始节点,从这些节点开始根据引用关系向下搜索,搜索过程中所走的路径成为“引用链”,如果某个对象到“GC Roots”之间没有任何的引用链相连,或者用图论的话来说从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

image.png

如上图,object 5、object 6、object 7虽然相互之间有关联,但是GC Roots不可达,所以他们就是要被回收的对象

哪些对象被作为GC Roots呢(来自《深入立即Java虚拟机-JVM高级特性与最佳实践(周志明版)》)

(1)在虚拟机栈(栈桢中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量表、临时变量等;

(2)在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;

(3)在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用;

(4)在本地方法栈中JNI(Native方法)引用的对象;

(5)Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器

(6)所有被同步锁(synchronized关键字)持有的对象

(7)反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

不管是引用计数算法还是可达性分析算法,判断对象的存亡都与引用有关,而引用根据强度还可分为四种类型

(1)强引用:常见的“Object object = new Object()”这种引用关系,就是强引用。无论任何情况下,只要强引用关系还在,该对象就不会被回收;

(2)软引用:当内存不够,即将发生内存溢出时,该类对象将会被回收。可以使用SoftReference类来实现软引用;

(3)弱引用:无论内存是否足够,只有有进行垃圾回收,弱引用关联的对象都会被回收。可以用WeakReference来实现;

(4)虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动

特殊情况:

可达性分析算法中判定不可达的对象不会立即被回收,这些对象需要经历两次标记才能“死亡”

可达性分析之后未与GC Roots关联的对象将会被第一次标记;

然后会根据是否有必要执行finalize()方法去进行一次筛选;

有必要执行的将会放到一个队列中,然后等待执行finalize()方法

finalize()中对象只要重新与GC Roots上的引用链上的对象建立关联,那么在第二次标记时,将会被移除“即将回收”集合,如果对象没有被引用,将会被第二次标记,此时将会被正式宣告死亡,然后被回收掉

注意:finalize()方法只会被系统调用执行一次

知道了哪些对象会被回收之后,接下来就是怎么回收这些垃圾了

分代收集算法:

在Java虚拟机中,堆内存一般分为两个部分,新生代和老年代(比例为新生代:老年代=1:2),新生代又分为Eden区、S0区和S1区(Eden:S0:S1=8:1:1),如需详细了解内存结构可参考深入理解JVM-JVM内存模型(二),这样分代的原因是因为大多数对象都是“朝生夕死”的,在内存中的存活时间并不长,在新生代中就已经完成了其使命,然后部分对象经过S0区与S1区之间交换,一部分被回收,一部分晋升到老年代。分代收集算法实际上是垃圾回收的基础框架,以此框架,根据不同区域的对象特征,采用不同的收集算法来执行GC过程

标记-清除算法:

顾名思义,标记、清除是该算法的两个步骤先标记所有需要被回收的对象,然后统一回收掉所有被标记的对象;或者标记所有不需要被回收的对象,然后统一回收掉所有未被标记的对象。标记过程其实就是对象是否属于垃圾的过程

主要缺点:

(1)执行效率不稳定,如果堆中有大量对象,且其中大部分是需要被回收的,这样就需要大量的标记和清除动作,其效率便会降低

(2)内存碎片化问题,标记清楚之后,内存空间中会出现大量的不连续的内存碎片,会导致需要为较大对象分配内存空间时,没有足够大的连续空间分配,导致再一次的垃圾收集

image.png

标记-复制算法

该算法的原理是将内存容量按照对象的特征分为大小相等的两块,每次只使用其中一块。当一块的内存使用完之后,对这块内存进行标记处理,将存活对象移到另一块内存上,再将当前内存块上的被标记对象一次性清除掉;如果存活对象较多,会产生大量复制操作,此时则将被标记对象移到另一块内存,并进行清除

优点:高效、简单

缺点:存在一定的内存浪费

新生代中的S0区和S1区就是这样的一种算法实现

image.png

标记-整理算法

该算法的原理和标记-清除算法的原理相似,但是其标记后的处理与标记-清除不同,标记-清除算法标记完成之后,直接将被标记的对象清除,标记-整理算法标记完成之后,是将存活对象向内存空间一端移动,然后清理掉边界以外的内存

如果移动存活对象,对于老年代这种大量对象存活区域,移动对象并更新所有引用这些对象的地方,必须全程暂停用户应用程序,这种暂停也叫做“Stop The World”

image.png

Java的自动内存管理,本质上是自动解决两个问题:自动给对象分配内存和自动回收分配给对象的内存

前面已经说过内存回收,下面说下内存分配

对象的内存分配从概念上讲,应该都是堆上分配(实际上也会有栈上分配,需要自行去寻找下答案,此篇不做解释)

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden区分配,当Eden区没有足够空间进行分配时,虚拟机会发起一次Minor GC

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

大对象指的是需要大量连续内存空间的Java对象,大对象的复制操作将会占用高额的内存复制开销,应尽量避免,HotSpot虚拟机提供了 -XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样是为了避免在Eden区及两个幸存者区之间来回复制,产生不必要的开销

3、长期存活的对象都将进入老年代

对象在Eden区产生,在新生代每经历一次Minor GC,其年龄+1,当该对象的年龄计数器超过一定的阈值(默认15)时,其将进入老年代。阈值可通过参数 +XX:MaxTenuringThreshold设置

4、动态对象年龄判定

HotSpot虚拟机中并不是永远要求对象年龄达到阈值才能晋升老年代,如果在幸存者空间中低于或等于某个年龄的所有对象大小的总和大于幸存者空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,不用等到阈值

5、空间内存分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果满足,则此次Minor GC可以确保是安全的;如果不成立,则虚拟机会先查看是否设置 -XX:HandlePromotionFailure参数设置的值是否允许担保失败;如果允许,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将会进行一次Minor GC,尽管是有风险的;如果小于,或者 -XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC

以上内容为部分知识总结,如需详细了解,请参看《深入理解Java虚拟机-JVM高级特性与最佳实践(周志明第三版)》

小贴士:JVM相关内容如需深入了解,可在B站观看尚硅谷宋红康老师的视频,然后再搭配周志明老师的《深入理解Java虚拟机-JVM高级特性与最佳实践(周志明第三版)》,视频内容是与书籍相同步的,两者共同食用效果更佳

推荐下自己的公众号,最新文章即时获取

image.png