• java虚拟机入门(四)-垃圾回收的故事


      谈到垃圾回收器,java程序员骄傲了起来,c语言你是够快,但是你有管家帮你打扫吗,还不是得靠自己的一双手,有钱就是任性。既然如此令java程序员骄傲的垃圾回收器,怎能让人不想去一探究竟呢!

      垃圾回收器字面意思就是回收垃圾的,那么对于程序来说,什么事垃圾呢,怎样定位垃圾呢,我们来看一下:

    一、什么是垃圾

      垃圾简单来说,就是废弃的东西,我不需要了,他就是垃圾,记得之前看言情剧总有一句台词,就算是被全世界嫌弃,但是在我这里依然是宝贝,同理,如何定义垃圾,众说纷纭,对于程序来说,最常见的垃圾回收器主要还是:引用计数法,可达性分析。

      1.引用计数法:

        引用计数法就是对每个对象的引用进行计数,有一个引用指向它就加1,当计数为0表示没有引用,这就是个垃圾。

      2.可达性分析

        可达性分析理解起来稍微复杂一点,核心就是根(GC ROOT)可达,从根节点出发,向下发散搜索,所有能引用到的对象才是宝贝,其他的都是垃圾。那么问题来了,根节点到底是啥:

        可以作为根节点的对象:

        1. 虚拟机栈中(本地变量表)引用到的对象,如各个线程堆栈使用的参数,局部变量,临时变量等,其实就是当前还被线程持有的变量,

        2. 方法区中类静态属性引用到的对象如java类的静态变量

        3. 方法区中常量对象的引用,如字符串常量池的引用

        4.本地方法栈中引用的对象,即我们的native方法。

        5.java内部引用,如class对象、异常对象(NullPointException,OutofMemoryError等),系统类加载器

        6.同步锁synchronized持有的对象

        7.JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等

        8.JVM 实现中的“临时性”对象,跨代引用的对象

        说实话我也不怎么记这个东西,我觉得要我背的东西都不是好东西,所以我理解的话,主要还是理解前面几个如虚拟机栈中局部变量表持有对象,那么局部变量表里面有的对象又是啥呢,线程在使用的对象,那么自然是不能被回收的,静态变量属于类级别的,这种对象是需要可以直接被使用的,自然也不能被回收,常量我理解和静态变量一样,native方法那不是我们能管的,自然也不能被回收,其实理解了也就是线程在用的不能回收,随时要被用的不能回收,native不归你管,你凭啥回收呢,不用背,自然能记得。

      看了上述两种方法,让你选你会选哪个,你可能会选择第一个,毕竟简单,但是咱们再来看一下,第一种垃圾分类的方法,如果我有俩对象,虽然都不用了,但是你拉着我,我拉着你,自然是不能被回收的,但是其实呢,已经不用了,一个固然无所谓,但是程序在运行是,可能会出现无数个这样的状况,而且这种对象你很可能永远都无法回收,越积越多,多恐怖的一件事,所以你必然是需要其他方式来解决这种问题,这么一想好像也没那么简单,但是第二种虽然听起来麻烦一点,但是确实不会出现这样的问题,也不需要任何辅助。好了如果是你,现在你会选哪个呢。

     

    二、垃圾回收基础

      1.丐帮秘诀基础篇 --- 什么是GC

        GC全称Garbage Collection,翻译过来就是垃圾回收,在java中其实就是自动化的垃圾回收机制

        我们一般关注的GC主要是针对堆中进行回收,栈中由于是随线程自动回收,方法区的回收效率极低,因此不需要太多关注,毕竟还是回收对象嘛,对象在哪呢,自然是堆中。

      2.丐帮秘决晋级篇 --- 垃圾分代

        

        垃圾收集器一般分为新生代和老年代,新生代又分为Eden区和两个survior区,至于为什么分呢,首先我们要了解一下对象的GC分配策略:

          1.栈上分配--- 判断当前对象可逃出当前方法,其实就是判断当前对象是否有可能在任何其他方法中有可能被修改,任意如作为入参传入其他方法,作用域不仅限于当前方法等都算是逃出当前方法(其实这就是大名鼎鼎的逃逸分析),如果确定不会逃出方法,则可直接在栈上分配,相信看过前面文章的同学应该知道栈比堆要快很多,直接栈上分配可以显著提高jvm效率。

          2.对象优先在Eden区分配 --- 即对象会首先进入Eden区,如果Eden区分配满了,则会发起一次minorGc,为啥要这么做待会会细讲。

          3. 大对象直接进入老年代 --- 啥叫大对象呢,比如一个大的list,一个占用很大内存的数组,其实就是一个对象个体,但是占用的内存很大。

          4.长期存活对象进入老年代 --- 什么是长期存活对象呢,上节有说过,对象头里面有个记录GC年龄的,每次发生minorGc,年龄都会加一,hotsport默认是年龄达到15会进入老年代,当然,这个也是可以通过配置-XX:MaxTenuringThreshold 调整。  

          5.对象年龄动态判断 --- 如果在survior空间中,同意年龄对象总和大于survior空间的一半,年龄大于或等于这个年龄的对象可以直接进入老年代

          6.空间分配担保 --- 网上的解释话太多了,我总结一下就是对象从新生代准备进入老年代的时候,如果设置了允许空间担保, 则继续检查新生代最大可用连续空间是否大于历代晋级到老年代对象的平均大小,如果大于,则直接进入老年代,担保失败则需进行fullGc,而没有设置分配担保如果新生代总大小大于老年代最大可用连续空间,则需先fullGc。可以看到区别就是一个是担保失败fullGc,而另一个则是直接fullGc(这里新生代大于老年代最大可用连续空间是前提)。

        介绍完了对象分配策略,我们在回过头来看一下为啥分代要这么分,大家都知道对象是朝生夕死的,而分新生代和老年代就是要让一大部分对象在新生代就pass掉,而新生代又分为Eden和survior区,这样分完,很多对象在进行第一次minorGc时,就直接pass掉了,根本不需要进入survior区,这也是为啥那么对象要优先在Eden区分配,而大对象(可以设置参数-XX:PretenureSizeThreshold控制大小)直接进入老年代,如果当出现很多朝生夕死的大对象,那么由于没有新生代的过滤,老年代很快占满,就需要频繁FullGc,就会导致系统卡顿,这种确实没有什么好的办法解决,毕竟大对象新生代是装不了多少的,想想还是这么设计靠谱,大牛们果然不是盖的。

        GC分配策略好像是有点多,背肯定是会忘记的,所以还是要靠理解,比如上面说的为啥要这么分代,正是因为这么解决是比较合适的,理解了这么设计的原因,其实这些策略看起来更像是一种解决方案。

        

    三、垃圾回收算法

       垃圾回收算法主流的主要有三种,复制回收,标记清除,标记整理,评价之前我们先了解一下吧:

      1.复制回收

        复制回收算法强调复制两个字,首先会将内存分为两块,一部分存储,一部分用来复制,因此只有一半内存可用,流程如下图首先会将可用内存复制到另一半,之后清掉之前那一半内存,供下次复制使用。复制算法的优点就是快,直接复制过来,原来的直接清掉,而且不会产生内存碎片。

                                               

                                                    

               

                                                    

                         

      2.标记-清除(Mark-Sweep)

        标记清除算法分为两步,标记和清除,看起来就很麻烦,首先标记出所有需要回收的对象,之后清除回收掉之前标记的对象。标记清除相对于复制算法不需要额外的一份内存,但是会导致有内存碎片,如下图,可以发现很多断断续续的内存块,这种情况如果来一个比较大的对象,即便内存够用,但是没有足够大的连续内存,依然无法使用,此时就必须要再次触发垃圾回收。

                                 

                                                                        

                                

       

      3.标记整理(Mark-Compact)

        标记整理算法在我看来更应该叫标记-整理-清除算法,首先标记出所有可回收对象,随后将所有不可回收对象向另一边移动,之后清除掉可以范围外的垃圾,可以看出步骤很复杂,耗时明显会比前两种要长,但是相比复制算法,它不需要额外的内存,相比标记清除算法,它不会产生垃圾碎片。

                            

                      

                           

                      

           

      以上是常用的三种垃圾回收算法,复制算法明显的是用空间换时间,标记清除舍弃了部分空间内存连续性,而最后一种就是用时间换空间了。

       记得很久之前面试的时候,面试官问过我一个问题,新生代使用的是什么算法,我记得自己看过,就回答了复制算法,说实话当时也是连猜带蒙的,紧接着就是为什么,这个问题给我干蒙了,当时脑子里的想法就是,我哪知道为什么,你问设计这个算法的人啊。但是最后还是卑微的回答了句,不知道。我知道这个面试已经结束了,确实是实力不够,回来之后就把jvm这块系统的重新学了遍,回炉重造。

      还是回到问题,新生代为啥要用复制算法呢,有研究表明(确实是用大量数据得出的结果),98%以上的对象都是朝生夕死的,复制过去的对象只占用极少数,那么新生代为啥又要搞这么多分区,又是Eden又是survior,survior还整俩,我们梳理一下常规对象进入新生代的过程是怎样的:首先进入Eden区,随后Eden区满之后,会进行minorGc,可用对象会第一次进入survior区,而下一次survior区满之后,会再次进行minorGc,将对象从survior 1区复制到2区,再清掉1区,循环直到满足GC分配策略进入老年代。如果没有Eden区,则俩survior区都需要扩大很多,而复制算法必须要用俩同样大小的内存,因此俩survior区缺一不可,这时候可能有的小伙伴要说了,标记清除,标记整理不都可以吗,也不是不可以,但是它不够快,标记清除还会有残留内存碎片,标记整理效率太低,因此后两种更多地用于老年代的回收。

    总结:

      这一章主要还是介绍垃圾回收的概念,什么是垃圾,垃圾回收器是啥样的,最后垃圾回收器在回收的时候是怎么玩的,都是偏理论的知识,可能理解起来没那么容易(也是我没有写好),但是我相信看完也大概知道我们的垃圾回收器大概是个啥,其实我最想表达的不仅仅是垃圾回收器的分代是啥样的,我想写出的是为什么要这么设计,从对象朝生夕死的特点,我们也可以知道这种分代方式是比较合理的,又通过反证法,证明如果说少了Eden或者survior,都是有很多不必要的弊端,而在对象分配策略中,我们基本也可以通过对象的特点和分代模型,分析出这样设计的合理性,比如优先在Eden区分配是因为这样可以过滤掉绝大部分朝生夕死的对象,其实理解了这些,可能问你分配策略时你还是记不住哪些点,但是你对jvm调优这方面,绝对前进了了一大步。

  • 相关阅读:
    sum left join 多次引发的问题
    WPF之Binding基础三 Binding的路径Path
    解决Android Studio启动不了的办法
    指向常量的指针和常量指针的区别简单理解
    简单的字符串操作
    QT问答
    log4qt使用示例
    在线代码库
    Qt5的windows开放环境准备
    QT 调试技术
  • 原文地址:https://www.cnblogs.com/gmt-hao/p/13843250.html
Copyright © 2020-2023  润新知