JVM(二)——垃圾回收(GC Garbage Collection)

       在上一篇中 JVM(一)——Java内存区域管理 中我们介绍了JVM这个容器内存区域管理。其中 程序计数器、虚拟机栈、本地方法栈 为线程私有的,也就是这几个区域的内存占用,随线程而生、随线程而亡。而 Java堆 和 方法区 则不一样,一个接口中的多个实现类需要的内存不一样,一个方法中的多个分支需要的内存也不一样,只有程序在真正运行执行的期间才能知道创建哪些对象,动态创建、动态回收,也就是今天要说的垃圾收集器主要做的事。

      好,说到对象回收,Carbage Collection,我们需要思考:1,哪些内存需要回收?2,什么时候回收?3,如何回收?

      下边,先看下思维导图吧,从1,如何判断对象是否可以回收;2,GC原理算法是什么?3,有哪些GC收集器?4,如何分配内存,如何收回就提高性能了 的顺序、方面进行学习总结。

       一,对象死了么?在堆里存放着Java几乎所有的对象实例,垃圾收集器进行回收前,首先需要判断对象的“死活”(是否被使用)。好看下下边两种判断的算法:

       1,引用计数算法(Reference Counting):给对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的。不足:很难解决对象之间相互循环引用的问题。

       2,可达性分析算法(Reachability Analysis):通过一系列成为“GC Roots”的对象作为起始点,向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(也就是从GC Roots到这个对象不可达),则证明此对象不可用。(很容易理解:谁和GC Roots有链连着,则被用着,否则孤立了,就没用了)。

       在Java代码中,我们想下,哪些可以构成GC Roots:a,虚拟机栈(栈帧中的本地变量表)中引用的对象(理解:也就是我们编写方法代码,线程执行的);b,方法区中类静态属性引用的对象(static修饰的);c,方法区中常量引用的对象(string ,int等常量);d,本地方法栈中JNI  Native方法 引用的对象(类似虚拟机栈)  

       在 Java高并发(六)——ThreadLocal为线程保驾护航 中我们涉及到弱引用,这里无论上边哪种算法,都是按照是否引用来判断的。我们在GC这里对Java这四种引用再看下。

Java引用
1,强引用-Strong Reference 我们在代码经常写的Object obj = new Object(),只有强引用存在,GC永远不会回收掉此类对象
2,软引用-Soft Reference 描述一些还有用但是非必需的对象,只有在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中,进行二次回收,如果这次回收还没有足够内存,才会剖出内存溢出异常。SoftReference类可实现。使用可以看下:https://blog.csdn.net/liuhaiabc/article/details/74784061
3,弱引用-Weak Reference 也是描述非必需对象的,比软引用更弱一些,弱引用的对象只能生存在下一次GC之前,在GC时无论内存是否足够,都会将其回收掉。WeakReference可以实现。
4,虚引用-Phantom Reference

也称 幽灵引用或者幻影引用,为Java中最弱的一种引用关系。虚引用不会决定对象的生命周期,持有虚引用和没有持有一样,在任何时候都可能被GC 回收。 设置虚引用的唯一目的就是在这个对象被GC回收时收到一个系统通知。PhantomReference类可以实现。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

       可以看这篇文章更多了解:https://www.cnblogs.com/skywang12345/p/3154474.html

       二,垃圾收集算法:

垃圾收集算法
算法名称 说明 特点
1,标记-清除 最基础收集算法,分为标记和清除两个阶段:首先标记处所有需要回收的对象,标记完成后统一回收所有被标记的对象。

1,效率,标记、清除两个过程效率都不高;

2,空间,标记清除后会产生大量的内存碎片,空间碎片太多可能会导致以后大对象无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

2,复制 将内存按容量划分为大小相等的两块,每次只用其中一块,当这一块内存使用完了,就将还存活着的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。

1,内存分配无需考虑内存碎片问题;

2,只要移动对顶指针,按顺序分配内存即可,实现简单,运行高效;

3,代价,内存缩小为了原来的一半;

4,存活率较高时,复制操作将降低性能。(老年代就不适合)

3,标记-整理 标记过程和标记-清楚算法一样,然后让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。 1,适合存活率高,老年代区的垃圾收集算法。
4,分代收集 Java堆根据对象的存活周期,分为老年代(存活周期长),新生代(又分为Eden(空间大、对象周期最短),两块Survivor),根据不同区域选择上边适当的收集算法,提高性能。

1,分类使用上边的算法,根据区域特点选择适合自己的收集算法;

2,存活对象位置:eden——>survivor1——>survivor2——>老年代,随着GC,对象一直存活,则不停改变自己的位置。

       小结:JVM的垃圾收集,举个例子吧,生活中小朋友玩玩具:1,买了很多玩具玩(eden空间大进行存放);2,玩一遍后,90%的没兴趣了直接扔掉了,剩下10%小朋友还继续玩着(survivor1);3,又玩了一段时间,又有5%不想玩了,直接舍弃了,剩下%5还在玩着(survivor2);4,又玩了一段时间,最后只对一个变形金刚感兴趣了(进入老年代),其它都舍弃了;5,最后变形金刚玩了好长时间,也玩完了,怎么办?重新买。理解,对应,不同阶段,不同收集,根据存在周期长短、数量大小,进行分代存放,回收。

       三,垃圾收集器:   

       收集算法是内存回收的策略方法论,下边要讲的垃圾收集器是内存回收的具体实现,先看总览:

       看下各个垃圾收集器的运行原理图:

       1,Serial/Serial Old收集器运行图

       2,ParNew/Serial Old收集器运行图

       3,Paraller Scavenge/ParallerOld收集器运行图

       4,CMS收集器运行图

       5,G1收集器运行图

       下边,我们通过对比表,看看各个收集器。 

垃圾收集器
收集器名称 描述 特点
1-Serial

当进行垃圾收集时需要暂停其他工作线程,理解:工作一段时间,停下来收集一下垃圾。

1-单线程、简单高效、新生代适用;

2-复制算法,暂停所有用户线程。

2-ParNew Serial的多线程版本,关注让用户线程停顿时间尽可能缩短

1-多线程、多核机器下性能优于Serial、新生代适用;

2-复制算法,暂停所有用户线程。

3-Parallel Scavenge

类似ParNew,但是关注让吞吐量(Throughput)可控制,降低;

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

1-多线程、新生代适用;

2-复制算法,暂停所有用户线程。

4-Serial Old Serial的老年代版本

1-单线程、简单高效、老年代适用;

2-标记-整理算法,暂停所有用户线程。

5-Parallel Old Parallel Scavenge 老年代版本

1-多线程、老年代适用;

2-标记-整理算法,暂停所有用户线程。

6-CMS(Concurrent Mark Sweep)

以获取最短停顿时间为目标的收集器;

步骤:1,初始标记(仅仅只是标记一下GC Roots能直接关联到的对象,速度很快);2,并发标记(进行GC Roots Tracing过程);3,重新标记(修正并发标记期间用户程序继续运行导致标记产生的变动的部分);4,并发清除。其中1和3(耗时都相对较少)为停止所有用户线程;2和3(主要耗时阶段)为并行处理

1-单线程、多线程、低停顿、老年代适用;

2-标记-清除算法,与用户线程串行、并行结合处理。

7-G1

G1将Java堆的划分为多个大小相等的独立区域Region,G1跟踪各个Region里边垃圾堆积的价值大小,在后台维护一个垃圾回收优先列表,每次优先回收价值最大的Region。

步骤:1,初始标记;2,并发标记;3,最终标记;4,筛选回收

1-并发与并发:充分利用多喝CPU的硬件优势,缩短停顿时间;

2-分代收集:分成独立的区域,进行复制,分区收集

3-空间整合:利用标记-整理算法;

4-可预测的停顿,可进行设置。

5-单线程、多线程、各个Region都适合

       看下关于垃圾收集器的参数设置吧:https://blog.csdn.net/tanga842428/article/details/52303643

       垃圾回收过程中:1,可达性分析判断对象是否该回收,JVM利用oopMap提前存好引用信息,而不是实时的判断分析(大大减少停顿时间);         2,安全点-safePoint,记录oopMap的暂时不变或者安全点,进行GC,防止不对变化的引用引起更多的麻烦;         3,安全区域-safe Region,用户线程进入时,标记safe Region,等待,完成GC后,出来继续执行。

       四,内存分配和回收策略:总结了各种垃圾回收算法,收集器,我们看下如何分配,能够和回收结合达到高效处理。

       1,对象优先在Eden分配:大多数对象生存周期短,gc频繁,在Eden分配处理,随着GC的次数,我们不断为生存周期更长的survivor区,老年代转化即可;

       2,大对象直接进入老年代:例如很长的字符串和数组,避免在新生代里频繁的复制;

       3,长期存活的对象将进入老年代:虚拟机给每个对象定义了一个对象年龄计数器,通过gc的不断进行,长期存活的对象年龄不断增大,最后像上边的例子所说的一样,进入到老年代;

       4,动态对象年龄判定:3中随着gc的不断进行年龄的不断增加,达到一定值进入老年代,是在空间足够的情况下,如果空间限制的话,也会根据实际情况,将最大年龄的进行复制转移,及时没达到设置的值。例如:在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

       5,空间分配担保:在发生Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,那么Minor GC安全。如果不成立,并且设置了HandlerPromotionFailure为允许担保失败,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年对象的平均大小,如果大于,将尝试这次Minor GC,尽管有风险。如果小于,或者设置为不允许,则改为进行一次Full GC。(复制算法带来的)

       好,上篇学习了JVM的内存区域,这篇我们总结了JVM中非常重要的GC,自动的垃圾回收让我么开发方便了很多,但是我们要明白其运行原理,方便出了问题进行寻找排查,方便更好的调优并充分利用。继续……

猜你喜欢

转载自blog.csdn.net/liujiahan629629/article/details/85215655
今日推荐