赞
踩
Java是如何实现垃圾回收的呢?简单来说,垃圾回收算法要做的有两件事:
本质上后续所有的垃圾回收算法,都是在上述两种算法的基础上优化而来。
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为Stop The World简称STW,如果STW时间过长(系统假死)则会影响用户的使用。
如下图,用户代码执行和垃圾回收执行让用户线程停止执行(STW)是交替执行的。
交替执行过程可以通过如下代码验证:
package chapter04.gc; import lombok.SneakyThrows; import java.util.LinkedList; import java.util.List; /** * STW测试 */ public class StopWorldTest { public static void main(String[] args) { new PrintThread().start(); new ObjectThread().start(); } } /** * 打印线程 */ class PrintThread extends Thread{ @SneakyThrows @Override public void run() { //记录开始时间 long last = System.currentTimeMillis(); while(true){ long now = System.currentTimeMillis(); // 如果不是垃圾回收影响,这里每次都应该是输出100 System.out.println(now - last); last = now; Thread.sleep(100); } } } /** * 创建对象线程 */ class ObjectThread extends Thread{ @SneakyThrows @Override public void run() { List<byte[]> bytes = new LinkedList<>(); while(true){ // 最多存放8g,然后删除强引用,垃圾回收时释放8g if(bytes.size() >= 80){ // 清空集合,强引用去除,垃圾回收器就会去回收对象 bytes.clear(); } bytes.add(new byte[1024 * 1024 * 100]); Thread.sleep(10); } } }
代码运行之前,设置如下JVM参数
所以判断GC算法是否优秀,可以从三个方面来考虑:
吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高。
最大暂停时间指的是所有在垃圾回收过程中的STW时间最大值。比如如下的图中,黄色部分的STW就是最大暂停时间,显而易见上面的图比下面的图拥有更少的最大暂停时间。最大暂停时间越短,用户使用系统时受到的影响就越短。
不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
上述三种评价标准:堆使用效率、吞吐量,以及最大暂停时间不可兼得。
一般来说,堆内存越大,回收对象就越多,最大暂停时间就越长。想要减少最大暂停时间,就要减少堆内存,少量多次,因为每次清理有一些准备工作,因此垃圾回收总时间会上升,吞吐量会降低。
没有一个垃圾回收算法能兼顾上述三点评价标准,所以不同的垃圾回收算法它的侧重点是不同的,适用于不同的应用场景(即垃圾回收算法没有好与坏,只有是否适合)
标记清除算法的核心思想分为两个阶段:
第一个阶段,从GC Root对象开始扫描,将对象A、B、C在引用链上的对象标记出来:
第二个阶段,将没有标记的对象清理掉,所以对象D就被清理掉了。
优点:实现简单,只需要在第一阶段给每个对象维护标志位(在引用链上,标记为1),第二阶段删除标记值为0的对象即可。
缺点:
复制算法的核心思想是:
对象A首先分配在From空间:
在垃圾回收阶段,如果对象A存活,就将其复制到To空间。然后将From空间直接清空。
完整的复制算法的例子:
1、将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象。
2、GC阶段开始,将GC Root搬运到To空间
3、将GC Root关联的对象,搬运到To空间
4、清理From空间,并把名称互换
优点:
标记-整理算法
少了一次遍历的过程,因而性能较好;但是性能不如标记-清除算法
,因为标记清除算法不需要进行对象的移动缺点:
标记整理算法是对标记清理算法中容易产生内存碎片问题的一种解决方案。
核心思想分为两个阶段:
优点:
缺点:
现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。分代垃圾回收将整个内存区域划分为两块大区:年轻代、老年代:
可以通过arthas来验证下内存划分的情况:
-XX:+UseSerialGC
参数使用分代回收的垃圾回收器,运行程序。可以设置的虚拟机参数如下
参数名 | 参数含义 | 示例 |
---|---|---|
-Xms | 设置堆的最小和初始大小,必须是1024倍数且大于1MB | 比如初始大小6MB的写法: -Xms6291456 -Xms6144k -Xms6m |
-Xmx | 设置最大堆的大小,必须是1024倍数且大于2MB | 比如最大堆80 MB的写法: -Xmx83886080 -Xmx81920k -Xmx80m |
-Xmn | 新生代的大小 | 新生代256 MB的写法: -Xmn256m -Xmn262144k -Xmn268435456 |
-XX:SurvivorRatio | 伊甸园区和幸存区的比例,默认为8:如新生代有1g内存,则伊甸园区800MB,S0和S1各100MB | 比例调整为4的写法:-XX:SurvivorRatio=4 |
-XX:+PrintGCDetailsverbose:gc | 打印GC日志 | 无 |
老年代大小不需要设置,因为新生代设置完之后,老年代的大小就确定了(总的堆内存-新生代内存)
注
:如果使用其他版本的JDK,或者使用其他回收器,上面的部分参数可能就不会生效
代码:
package chapter04.gc; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 垃圾回收器案例1 */ //-XX:+UseSerialGC -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio=3 -XX:+PrintGCDetails public class GcDemo0 { public static void main(String[] args) throws IOException { List<Object> list = new ArrayList<>(); int count = 0; while (true){ System.in.read(); System.out.println(++count); //每次添加1m的数据 list.add(new byte[1024 * 1024 * 1]); } } }
使用arthas的memory展示出来的效果:
heap展示的是可用堆。
1、分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
2、随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区(算法使用的是复制算法)。Minor GC结束之后**,**Eden区会被清空,后面创建的对象又可以放到Eden区。
3、接下来,S0会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC。
此时会回收eden区和S1(from)中的对象,并把eden和from区中存活的对象放入S0。
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
4、如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
5、当老年代中空间不足,无法放入新的对象时,先尝试minor gc(为啥?**因为young满了之后,部分对象年龄没有到15,也被放在了老年区,**minor gc可以清理young区来放新对象)。如果空间还是不足,就会触发Full GC(停顿时间较长),Full GC会对整个堆进行垃圾回收。如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
下图中的程序为什么会出现OutOfMemory?
从上图可以看到,Full GC无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。
【测试代码】
//-XX:+UseSerialGC -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio=3 -XX:+PrintGCDetails
public class GcDemo0 {
public static void main(String[] args) throws IOException {
List<Object> list = new ArrayList<>();
int count = 0;
while (true){
System.in.read();
System.out.println(++count);
//每次添加1m的数据
list.add(new byte[1024 * 1024 * 1]);
}
}
}
结果如下:
老年代已经满了,而且垃圾回收无法回收掉对象,如果还想往里面放就发生了OutOfMemoryError
。
为什么分代GC算法要把堆分成年轻代和老年代?首先我们要知道堆内存中对象的特性:
分代GC算法将堆分成年轻代和老年代主要原因有:
复制
算法;老年代可以选择标记-清除
和标记-整理
算法,由程序员来选择灵活度较高。该文章是本人学习 黑马程序员 的学习笔记,文章中大部分内容来源于 黑马程序员 的视频黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 黑马程序员 的优质课程表示感谢。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。