《Java 虚拟机原理》5.1 GC垃圾收集及案例分析
时间:2023-06-27 15:45:01 | 来源:网站运营
时间:2023-06-27 15:45:01 来源:网站运营
《Java 虚拟机原理》5.1 GC垃圾收集及案例分析:
一、GC什么对象GC的对象是没有存活的对象,判断没有存活的对象有两种常用方法:
引用计数和可
达性分析。
1.1 java的GCRoots引用对象在 Java 虚拟机的语境下,垃圾指的是
死亡的对象所占据的堆空间。
a. 虚拟机栈中引用的对象。
b. 方法区中静态属性引用的对象。
c.方法区中常量引用的对象。
d.本地方法中JNI引用的对象。
说明:当前对象到GCRoots中不可达时候,即会满足被垃圾回收的可能。这些对象但不是就非死不可,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:
第一次:对象可达性分析之后,发现没有与GCRoots相连接,此时会被第一次标记并筛选。
第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为没必要执行。
1.2 结合GC对象回顾java虚拟机内存说明:a. 虚拟机栈中引用的对象、b. 方法区中静态属性引用的对象(b.1基本类型数据是存储在运行时常量池)、c.方法区中常量引用的对象,d.本地方法中JNI引用的对象,这些对象都存储在java堆。
图1 GC对象在java虚拟机内存图
二、什么时候GC2.1 判断没有存活的对象有两种常用方法如何辨别一个对象的存亡是关键问题。
1.
引用计数每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
优点:实现简单,判定效率高效,被actionscript3和python中广泛应用。
缺点:无法解决对象之间的循环引用问题。
图2 循环引用场景
2.
可达性分析目前 Java 虚拟机的主流垃圾回收器采取的是
可达性分析算法。该算法的实质:将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为
标记(mark)。最终,
未被探索到的对象便是死亡的,是可以回收的。
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的不可达对象。如下图所示,右侧的对象是到GCRoot时不可达的,可以判定为可回收对象。
图3 可达性分析
思考题:
什么是GC Roots?GC Roots与GC对象的关系?解答:
由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)下列几种,J
ava 方法栈桢中的局部变量、
已加载类的静态变量、
JNI handles、
已启动且未停止的 Java 线程。因此,
GC Roots是GC对象的引用。
可达性分析法的问题:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。误报使得Java 虚拟机损失该次垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。
2.2 触发GC的动作及时机(1)动作:程序调用
System.gc时可以触发。
(2)时机:
系统自身来决定GC触发的时机根据Eden区和From Space区的内存大小来决定,当内存大小不足时,则会启动GC线程并停止应用线程,GC又分为 Minor GC 和 Full GC。
Minor GC触发条件:① 当 Eden 区满时,触发 Minor GC。
② 当 FromSuv 或者 ToSuv 区满时,触发 Minor GC。
Full GC触发条件: ① 调用System.gc时,系统建议执行Full GC,但是不必然执行
② Heap 的老年区空间不足
③ Metaspace 空间不足
④ 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
⑤ 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
三、如何进行GCGC算法是内存回收的理论方法,而GC垃圾收集器则是是内存回收的具体实现。下面的内容先讲GC常用算法。
3.1 GC算法理论基础GC算法是内存回收的理论方法。GC常用算法理论有:
标记-清除算法,
标记-压缩算法,
复制算法,
分代收集算法。即回收垃圾对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。目前主流的JVM(HotSpot)采用的是分代收集算法。
3.1.1 标记清除法标记清除法是垃圾回收算法的思想基础。标记清除算法将垃圾分为两个阶段:标记阶段和清除阶段。
标记阶段:通过根节点,标记所有从根节点开始的可达对象,未标记过的对象就是未被引用的垃圾对象。
清除阶段:清除所有未被标记的对象。
图4 标记清除算法
3.1.2 复制算法复制算法是,将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在适用的内存中存活对象复制到未使用的内存块,然后清除使用的内存块中所有的对象。
图5 复制算法
3.1.3 标记压缩算法标记压缩算法是一种
老年代的回收算法。
标记阶段:与标记清除算法一致,对可达对象做一次标记。
清理阶段:为了避免内存碎片产生,将所有的存活对象压缩到内存的一端。
图6 标记压缩算法
四、Java虚拟机的堆划分Java 虚拟机将堆划分为
新生代和
老年代。其中,新生代又被划分为
Eden 区,以及两个大小相同的 Survivor 区即
FromSuv和
ToSuv。
当调用
new 指令时,java虚拟机在Eden区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在Eden区是需要进行
同步的。new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。
问题1:两个线程同时new Object1对象,则堆如何划分内存?
解答:由于堆内存是线程共享的,同步为两个线程分别划分object1的内存空间,即有2个object1对象。该技术被称为
TLAB(
Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。
问题2:当 Eden 区的空间耗尽了怎么办?
解答:这个时候Java虚拟机会触发一次
Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到
Survivor区。
问题3:新生代的两个Survivor 区,即
FromSuv和
ToSuv有什么用处?
解答:当Minor GC时,Eden和FromSuv中的存活对象会被复制到ToSuv中,然后交换FromSuv和ToSuv指针,以保证下一次Minor GC时,ToSuv还是空的。满足两种情况之一,可以使对象移动到老年代:
1. Minor GC,存活对象从FromSuv复制到ToSuv,其对象的age+1,当超过
(默认值)15的时候,转移到老年代;
2. 动态对象,
如果survivor空间中相同年龄所有的对象大小总和,大于survivor空间的一半,则年级大于或等于该年级的对象就可以直接进入老年代。
注意:
Minor GC只针对新生代进行垃圾回收,所以在枚举 GC Roots 的时候,需要
考虑从老年代到新生代的引用。为了避免扫描整个老年代,Java 虚拟机引入
卡表(Card Table)的技术,大致地
标出可能存在老年代到新生代引用的内存区域。
五、GC案例分析从一个
object1分析该对象在分代垃圾回收算法中的
回收轨迹。
Minor GC是指发生在新生代的GC,因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
Full GC是指发生在老年代的GC,出现Full GC一般会伴随至少一次的Minor GC,其速度一般比Minor GC慢10倍以上。
步骤1:实例化object1,出生于新生代的
Eden区域;
步骤2:
Minor GC,object1移动到新生代的
Fromsuv区域,object1还存活。
步骤3:
Minor GC,通过复制算法将object1移动到新生代的
ToSuv区域,同时object1的年龄age+1,object1 依然存活;
步骤4:
Minor GC,在新生代的survivor区域中,与object1同龄的对象并没有达到survivor的一半。因此,通过复制算法将
FromSuv和ToSuv 区域进行互换,object1对象被移动到了新生代的ToSuv,object1 依然存活;
步骤5:
Minor GC,此时survivor中和object1同龄的对象已经达到
survivor的一半以上,object1被移动到了
老年代区域,object1 依然存活。
满足两种情况之一,都可以使object1对象移动到老年代:
1. Minor GC,存活于survivor 区域的object1对象的age+1,当超过(默认值)15的时候,转移到老年代;
注意:minor GC下,步骤2/3/4中的移动/复制全部Tosuv/Fromsuv区域的对象。2. 动态对象,
如果survivor空间中相同年龄所有的对象大小总和,大于survivor空间的一半,则年级大于或等于该年级的对象就可以直接进入老年代。
步骤6:
Full GC会触发stop the world。object1存活一段时间后,此时GC Roots不可达object1,而且此时老年代空间比率已经超过了阈值,触发了Full GC,此时object1被回收。
注意:object1 被回收的必要条件是 object1
不可达(GC Roots),即 object1 的引用是
弱引用。
以上的步骤采用
分代垃圾收集的思想,描述object1对象从存活到死亡的过程。
新生代:采用复制算法,老年代:采用标记-清除算法或者标记-整理算法。
stop the world是一种简单除暴的方式,即停止其他非垃圾回收线程的工作,直到完成垃圾回收。Java 虚拟机中的 stop the world 是通过
安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 stop the world 的线程进行独占的工作。安全点的初始目的并不是让其他线程停下,而是找到一个
稳定的执行状态。例如,
java执行某个JNI本地方法时,不访问Java对象、调用Java方法、返回至原Java方法,则Java虚拟机的堆栈不会发生改变,所以这段代码可以作为安全点。因此,Java虚拟机在这个安全点,可以同时进行垃圾回收和执行这段代码。