JVM垃圾回收总结

JVM垃圾回收:

  1. 判断对象是否可以回收:

引用计数法:

        只要一个对象被其他对象引用一次,计数就+1;当它取消了被引用,计数就-1。当它的引用数等于0的时候,就把它当成垃圾回收。

        弊端:循环引用的时候,会导致循环引用的双方计数器都为1,那么他们就一直不能被当成垃圾回收,会造成内存泄漏

 

    JVM采用的可达性分析算法:

        根对象:肯定不能被当成垃圾回收的就称为根对象

 

        可达性分析算法:如果一个对象被根对象直接或者间接的引用,那么这个对象就不能被回收;反之则会在将来被回收。

 

        原理:扫描堆中对象,看是否能沿着GC Root为起点的引用链找到该对象,找不到就是可以回收

 

        有哪些可以成为GC Root:使用MAT工具进行分析,使用jmap导出代码运行某一阶段的二进制bin文件,导入MAT分析。

        系统类:即核心类都能作为GC Root,比如Object、HashMap、String等类

        Native方法类本地方法的引用类也能作为GC Root

        Busy Monitor对象:即加了锁的对象可以作为GC Root,不然一旦清除将无法解锁

        线程类线程需要的对象可以作为GC Root,不然线程无法正常运行。但是栈帧(方法调用)的局部变量的引用对象都可以作为根对象,引用对象并不是对象,就像list = new ArrayList<>(),其中list就是引用对象。当list = null的时候,局部变量被回收,但是list没有被回收。

 

    五种引用:(无论是什么引用,它们本身就是一个对象)

        强引用:

            我们平时使用的赋值 A = B 就属于强引用。当一个对象被强引用时,它将不可能被回收。

        软引用:

            当一个对象被弱引用的时候,如果内存足够,将不会被回收;但是当内存不足够的时候,该对象就会被回收

            可以配合引用队列使用,当软引用对象被回收的时候,它就会回到自己所属的引用队列中。

            软引用的应用:

            软引用需要获取SoftReference<>对象,它里面是一个泛型,用于装载需要被软引用的类,还可以在new的时候绑定队列。当内存不够的时候,软引用所指向的内存空间就会被回收,即软引用了null,释放了资源。如果有队列ReferenceQueue就加入队列

            当使用软引用的时候,垃圾回收之后虽然还有SoftReference对象,但是它再也读取不到之前指向的地址了

 

        弱引用:

            比起软引用更短的生命周期。当程序执行了垃圾回收的时候,只要发现了弱引用的对象,无论内存空间够不够,都立刻把它回收。

可以配合引用队列使用,当弱引用对象被回收的时候,它就会回到自己所属的引用队列中。

弱引用的应用:

            弱引用需要获取WeakReference<>对象,它里面是一个泛型,用于装载需要被弱引用的类,还可以在new的时候绑定队列。当触发GC的时候,弱引用所指向的内存空间就会被回收,即弱引用了null,释放了资源。如果有队列ReferenceQueue就加入队列

            当使用弱引用的时候,垃圾回收之后虽然还有WeakReference对象,但是它再也读取不到之前指向的地址了

 

        虚引用(必须配合引用队列使用)

            虚引用就是形同虚设的引用。当一个对象持有虚引用的时候,无论什么时候都有可能被系统回收。它的唯一作用就是当对象被回收的时候返回一个系统通知

        终结器引用(必须配合引用队列使用)(不建议使用)

            所有Java对象都会继承一个Object父类,而Object类有一个finalize方法,也就是终结方法。当当前对象重写了finalize方法并且没有强引用的时候,就可以把它当成垃圾回收。当一个对象有终结器引用时,回收的时候会先把终结器引用加入引用队列中,再由一个优先级很低的线程FinalizeHandler来监听是否有对象被放入引用队列了,如果有就在下一次执行垃圾回收的时候进行回收

        

        

        

2.垃圾回收算法(三种结合工作)

    2.1标记清除算法

        标记:寻找那些GC Root不可达的内存空间,标记为垃圾

        清除:把找到的垃圾的起始地址和结束地址记录下来,在下一次分配空间的时候可以进行覆盖也相当于删除,再也读取不到了

        优点:速度快;缺点:容易产生内存碎片,会造成内存溢出

    2.2标记整理算法(存活较多使用)

        标记:寻找那些GC Root不可达的内存空间,标记为垃圾

        整理:避免内存碎片问题,在删除的过程中,把每个非垃圾的数据往前移动,进行整理

        优点:避免了内存碎片的问题,使内存使用率更高缺点:牵扯移动数据,速度慢

    2.3复制算法(存活较少的时候才可以使用)

        开辟两个空间,一个是FROM,一个是TO。当发生GC的时候,FROM里面非垃圾的元素会复制到TO里面。此时把FROM全部数据删除,并交换FROM和TO的角色,即TO变成了FROM,FROM变成了TO。

        优点:不会产生内存碎片;缺点:开辟空间较大

3.分代回收

    什么是新生代和老年代?(注意:新生代和老年代都在堆中)

        JVM把划分成两块,分别是新生代和老年代

        新生代中保存了一些价值较低的对象,例如生命周期较短的对象。

        老年代保存了生命周期更长的对象。

 

        新生代里面又划分了三个区域:伊甸园(8),幸存区FROM(1),幸存区TO(1)

        

        当我们创建一个新的对象的时候,都会把它放在伊甸园中。伊甸园放着放着放不下了,就会触发一次Minor GC,对伊甸园中的元素进行复制删除算法,把非垃圾存放到幸存区TO,交换TO和FROM,并清除伊甸园中的垃圾。被放入幸存区TO的数据的年龄会+1,当它超过了一个阈值(最大是15次)或者新生代怎么都放不下了的时候,就会被加入老年代。如果新生代和老年代都放不下了,就会触发Full GC,使得新生代和老年代都进行垃圾回收

    大对象直接晋升机制:

        JVM通过计算如果发现一个新的对象在新生代怎么都放不下了,如果老年代还放得下,就直接晋升到老年代,并不需要等阈值。如果老年代也放不下,就抛出OOM错误。        

    GC类型:

        Minor GC:仅针对新生代的GC

            Minor GC在触发的时候,把伊甸园中的非垃圾复制存放入幸存区TO,交换FROM和TO,并使得放入幸存区TO的数据年龄+1

            Minor GC会触发Stop the world:也就是说当它在进行垃圾回收的时候,其他线程将停止。等到垃圾回收完成后,再允许其他用户线程执行。因为Minor GC涉及到对象的复制问题,所以当GC的时候需要停止其他用户线程。

        Full GC:针对新生代和老年代的GC

            当老年代空间不足,会先进行一次Minor GC,但是如果之后空间依然不足,就会触发Full GC。

Full GC也能触发Stop the world

            如果Full GC之后也不能存放数据,就会抛出异常

    线程之间的OOM错误:

        如果在子线程里面抛出一个OOM错误,它会直接把子线程占用的空间立即释放掉,并不会影响主线程的正常工作。

 

4.垃圾回收器

    串行回收器:

    由于是串行的,即表明它是一个单线程,适合堆内存较小而且CPU内核数小的计算机,例如个人电脑。

    吞吐量优先回收器:

    吞吐量优先回收器是多线程的。适合堆内存较大的场景,需要多核CPU支持

使得单位时间内,Stop the world的时间尽可能地短

类比吃饭就是一口尽量吃的多,少餐多食

对于吞吐量优先回收器,它的实现是并行的。当各个CPU在处理到某个需要GC的点的时候,会跳入安全点内,然后数个CPU共同执行垃圾回收线程,阻塞用户线程。这时CPU占用率会暴涨,不过一次性处理的垃圾较多。

    响应时间优先回收器:

    响应时间优先回收器是多线程的。适合堆内存较大的场景,需要多核CPU支持

    使得单次的Stop the world的时间尽可能地短

类比吃饭就是尽量吃的快,少食多餐

 

    对于响应时间优先回收器,它的与其他用户线程运行是并发的,有可能造成内存碎片。当需要进行垃圾回收的时候,某个CPU会进行初始标记,这是用于标记GC Root对象的,并阻塞用户线程。然后解放用户线程,自己GC Root寻址(并发标记)。并发标记完成后,由于用户进程之前运行的时候可能产生了垃圾,所以进行一次重新标记。最后由垃圾删除线程并发清除垃圾

    有可能会残留垃圾:在并发清除的同时,其他线程产生的垃圾需要等到下一次GC才能清除。

 

    G1回收器:

        G1全称是Garbage First,整体上是标记+整理算法,两个区域之间使用复制算法

        JDK9开始成为默认回收器,并废弃CMS(响应时间回收器)回收器。内部是并发处理的,追求的是吞吐量和低延迟。适用于超大的堆内存。

        

        G1回收器工作流程:

        步骤1:Young Collection 新生代的垃圾收集

        当伊甸园被占满的时候,会触发一次新生代的垃圾收集,同时发生STW。当发生新生代的垃圾回收之后,幸存的对象会放入幸存区。当幸存的对象经历了一定周期之后或者幸存区内存不足的时候,也会触发一次新生代垃圾回收,晋升进入老年代的区域。

        步骤2:Young Collection + Concurrent Mark 新生代的垃圾回收+并发标记老年代的垃圾

        初始标记发生在Young GC,而当老年代在堆内存的储存到达一个阈值的时候,就会触发并发标记,把不在根对象下的垃圾进行标记。

        步骤3:Mix Collection 混合垃圾收集,同时进行新生代和老年代的垃圾收集

        在Mix Collection中,最终标记(即并发标记后的重新标记)会导致STW;拷贝存活也会导致SWT。

        对新生代和老年代内存区域进行垃圾收集,在移动数据的时候采用复制算法。Eden可能去了Survive或者老年代区等。

为了不超过最大GC暂停时间,Mix Collection会依次选择最有价值的老年代非垃圾数据,复制放入新的一片区域中,以减少内存碎片。

 

 

        各种回收器的Minor GC和Full GC的比较

            串行回收器:

新生代内存不足:minor gc

老年代内存不足:full gc

            

            并行回收器(吞吐量优先):

新生代内存不足:minor gc

老年代内存不足:full gc

 

            并发回收器(响应时间优先):

新生代内存不足:minor gc

老年代内存不足:

 

            G1回收器:

新生代内存不足:minor gc

老年代内存不足:老年代内存到达阈值,当回收速度比生产垃圾速度快的话,就只触发并发垃圾收集阶段;反之则触发full gc

 

        Young Collection跨代引用

        开发中会出现老年代的引用了新生代的情况,那么如果我们在可达性分析的时候就需要遍历整个老年代内存,来查看那些新生代对象是垃圾,这种设计显然是不合理的。

        老年代采用了一个叫卡表的技术,把老年代的区域再进行一次细分,每个细分的512B如果老年代其中有一个对象引用了新生代的对象,那么就标记为脏卡。在将来GC Root遍历的时候,就只需要遍历脏卡就可以了,减小了搜索范围,提高效率。

 

        Remark(重新标记阶段):

        上图在并行标记的时候,黑色表示已经处理完成,灰色表示正在处理,白色表示暂未处理。

        而箭头表示引用

        在并发标记的时候,当A -> X的引用断开了,那么X就会被标记成白色,A标记成黑色。但是由于是并发执行,B可以来一个强引用到X,但此时B为黑色表示不会再检查了,那就是说X还是白色。

        由于有以上情况,所以引入了重新标记。当发生强引用的时候,给X一个写屏障,并放入队列中,变为灰色到达重新标记阶段时,查看队列中是否有灰色,并对它进行GC Root判断它是否能被回收

        JDK8的字符串去重:

        在字符串进行创建的时候,会自动地放入一个队列中。当G1开始回收的时候,会并发的检查时候有字符串重复,如果有,那就让他们引用同一个地址。

        与String.intern方法不同,这个注重的是字符数组,String.inten注重字符串

        JDK8的类卸载:

        在JDK8u40,当G1在并发标记结束之后,就能知道那些类时不再被使用的。当类实例都被卸载且类加载器的所有类都不再使用,就把这个类加载器加载的所有类都卸载

 

        JDK8的回收巨型对象:

        当一个对象大小大于半个储存块的时候,就称它为巨型对象。

        G1不会对举行对象进行拷贝,而且回收的时候会优先考虑它

        G1会检测巨型对象被老年代引用的数量,当数量为0的时候,回收巨型对象。

        JDK9并发标记起始时间的调整:

        并发标记必须在堆空间占满之前完成,否则退化成Full GC。

        JDK9可以动态调整堆占用阈值,以令G1开始并发标记阶段。

5.GC调优(各个环境都不相同)

    通过java xx:-PrintFlagFinal可以查看当前使用的虚拟机参数

    由于GC会触发STW,会导致用户体验不好,所以我们需要使用调优

    调优目标:

    需要低延迟(响应时间快)还是高吞吐量?

    互联网项目就使用低延迟:CMS,G1,ZGC(JDK12)

    要科学运算就使用高吞吐量:ParallelGC

 

    最快的GC是不发生GC:

    如果系统发生比较多GC/Full GC,需要考虑是否数据量太多数据表示是否太臃肿,或者是否存在内存泄漏

    新生代调优:

    每一个线程都会在伊甸园中给它分配一个私有的空间TLAB(Thread-local allocation buffer),当new一个新对象的时候,就会在对应线程的私有空间中占用一片区域。

    新生代死亡对象代价是0大部分对象用完即死,同时Minor GC的时间远远低于Full GC的时间

        新生代内存越打越好吗?

        如果新生代的内存大了,那么老年代占用的就少了,那就是说会更加频繁的触发Full GC。并不是越大越好,最好在总堆内存大小的1/4-1/2,推荐新生代能容纳一次请求-响应中的所有对象*并发量,即 并发量*(请求-响应)中所有的数据。

        幸存区调优(JVM会通过幸存区大小调整晋升阈值)

        幸存区大小大到能保留当前活跃对象+需要晋升对象,当阈值配置得当的时候,可以让长时间存活的对象尽快晋升

        由于不快点晋升的话,会一直停留在新生代,导致会复制算法的时间会变长,响应时间也会变长。

    老年代调优:

    老年代使用CMS/G1进行垃圾回收的时候,当并发标记的时候会有额外的垃圾没有清理到位。如果浮动垃圾导致内存不足,会直接退化为SerialOld(串行垃圾回收器)

    优先调优新生代

    如果多次发生了Full GC,就将老年代内存预设调大1/4-1/3

    GC调优案例:

        Full GC和Minor GC频繁:

        空间紧张,创建对象很多的话,Minor GC就会多次触发。同时幸存区大小变小了,更多对象挤去了老年代。

        先尝试调优新生代空间,后尝试调优老年代空间。

        请求的高峰期发生了Full GC,单次暂停时间长:

        在初始标记和重新标记的时候,触发了STW,可以在重新标记之前调用一次Minor GC,使它需要重新标记的对象少了,这样STW时间也随之变短。

        老年代充裕的情况下,发生Full GC:

        JDK1.7之前使用永久代,如果永久代小了,就会导致Full GC。

        

        

 

附录:

    

posted @ 2022-03-28 16:59  Quent1nCn  阅读(111)  评论(0编辑  收藏  举报