zoukankan      html  css  js  c++  java
  • 垃圾收集器与内存分配策略

                                        垃圾收集器与内存分配策略

    一、了解垃圾收集的意义

       经过半个多世纪的发展,目前内存的动态分配与内存回收技术已经相当成熟,一切看起来已经进入了"自动化时代",那么我们为什么还要研究和去了解GC和内存分配呢。答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时候,我们就需要对这些自动化的技术实施必要的监控和调节。

    二、如何判断对象是否可回收

      1.引用计数法

         引用计数法就是给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效的时候,计数器的值就减1;任何时刻计数器的值为0就是不可用的。客观的说,引用计数法实现简单,判定效率高,在大部门情况下面是一个不错的算法,在Python语言里面有使用,但是java虚拟机里面并没有使用这种算法来管理内存,因为它很那解决对象之间循环引用的问题。

        下面写一组代码案例来加以说明:

        

    运行结果:

    [GC[DefNew: 2884K->441K(4928K), 0.0048536 secs] 2884K->2489K(15872K), 0.0049418 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

    [Full GC[Tenured: 2048K->439K(10944K), 0.0056467 secs] 4537K->439K(15872K), [Perm : 1678K->1678K(12288K)], 0.0057194 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

    Heap

     def new generation   total 4992K, used 180K [0x04800000, 0x04d60000, 0x09d50000)

      eden space 4480K,   4% used [0x04800000, 0x0482d370, 0x04c60000)

      from space 512K,   0% used [0x04c60000, 0x04c60000, 0x04ce0000)

      to   space 512K,   0% used [0x04ce0000, 0x04ce0000, 0x04d60000)

     tenured generation   total 10944K, used 439K [0x09d50000, 0x0a800000, 0x14800000)

       the space 10944K,   4% used [0x09d50000, 0x09dbdf30, 0x09dbe000, 0x0a800000)

     compacting perm gen  total 12288K, used 1685K [0x14800000, 0x15400000, 0x18800000)

       the space 12288K,  13% used [0x14800000, 0x149a5588, 0x149a5600, 0x15400000)

    No shared spaces configured.

    ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2

    JDWP exit error AGENT_ERROR_NO_JNI_ENV(183):  [../../../src/share/back/util.c:838]

    可以看到在GC前后内存发生了变化,证明了java虚拟机并没有使用引用计数法。

     2.可达性分析算法

      可达性分析算法的基本思路是:从一个根节点作为起始点,然后从这些根节点向下搜索,搜索所走过的路径成为引用链,当一个对象到达根节点没有任何引用链相连的话,也就是说该对象不可达,则证明此对象是不可用的,GC则会进行内存回收。

    如下图 GC Roots作为根节点,对象object5, object6, object7之间虽然有关联,但是因为他和GC Roots不可达,所以被判定为可回收对象。

     

    3.四中引用的概念说明

       强引用:是指在程序代码中普遍存在的,类似于Object obj = new Object()这类的引用,只要是强引用存在,垃圾回收器是永远都不能回收的对象。

       软引用:是用来描述一些还有用但不是必需的对象。对于软引用关联的对象,在系统中将要发生内存溢出异常之前,将会把这类对象列进回收范围进行二次的回收。如果二次回收以后,还没有足够的内存,才会抛出内存溢出异常。

       弱引用:也是用来描述非必须对象,但是他的强度只是比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够,都会回收掉这些弱引用对象。

       虚引用:他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生任何的影响,也无法通过一个虚引用来获取一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收的时候收到一个系统通知。

    4.对象的生存还是死亡

      即使在可达性分析算法中是不可达对象,也并非是非死不可的对象。要宣告一个对象死亡要经历两次标记过程:如果对象在进行可达性分析以后发现没有与GC Roots相连接的引用链,那么他会被第一次标记并进行一次筛选,筛选的条件是次对象有必要执行finalize()方法。当对象没有覆盖finalize()方法的时候或者finalize()方法已经被虚拟机执行过,虚拟机将这2中情况都视为没有必要执行。finalize()方法只会被系统调用一次。

       如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放在一个叫做F-Quequ的队列之中,并在稍后有一个虚拟机自动建立的、低优先级的Finalize()线程去执行他。这里所谓的执行是指虚拟机会触发这个方法,但不会承诺会等待他执行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,那么很可能会导致F-Queue队列中的其他对象处于永久的等待,甚至导致整个内存回收系统的崩溃。finalize()方法是对象逃脱死亡的命运的最后一次机会,稍后GC则会对F-Queue队列中的对象进行第二次标记,如果对象能够在重新与引用链中任何一个对象建立关联就可以成功的拯救自己,譬如把自己(this关键字)赋值给类变量或者自己成员变量的某一个属性。那么他在第二次标记的时候就会被移除即将回收的集合,但是如果这个阶段没有被移除即将回收的集合,那么他基本上就被回收了。

    通过以下实例代码进行说明:

    package com.gc.demp;
    
    public class FianlizeEscapeGC {
    
             public static FianlizeEscapeGC SAVE_SIGN = null;
    
             public void isAlive(){
    
                       System.out.println("yes , I am still alive");
    
             }
    
             protected void finalize() throws Throwable{
    
                       //调用finalize 方法 只能被系统调用一次
    
                       super.finalize();
    
                       System.out.println(" finalize method executed ");
    
                 //将自己赋值给类变量 在finalize的时候可以拯救自己
    
                       FianlizeEscapeGC.SAVE_SIGN = this;
    
             }
    
             public static void main(String[] args) throws Exception {
    
                       SAVE_SIGN = new FianlizeEscapeGC();
    
                       //因为finalize方法只会被系统调用一次 所以第一次拯救成功了
    
                       SAVE_SIGN = null;
    
                       System.gc();
    
                       //因为finalize的优先级比较低 所以暂停0.5秒来等待他
    
                       Thread.sleep(500);
    
                        if (SAVE_SIGN !=null){
    
                                 SAVE_SIGN.isAlive();
    
                        }else {
    
                                 System.out.println("no , I am  dead ");
    
                        }
    
                        //这段代码和上面的一样 但是第二次却失败了
    
                 SAVE_SIGN = null;
    
                       System.gc();
    
                       //因为finalize的优先级比较低 所以暂停0.5秒来等待他
    
                       Thread.sleep(500);
    
                        if (SAVE_SIGN !=null){
    
                                 SAVE_SIGN.isAlive();
    
                         }else {
    
                                 System.out.println("no , I am  dead ");
    
                        }
    
             }
    
    }

    运行结果:

     需要特别说明是的finalize()方法是java诞生之初为了C++程序员为了能接受他而做的一种妥协,他的运行成本很高,并不建议使用它来拯救对象。

    5.回收方法区

       方法区中主要是回收一些废弃的常量和无用的类。

    判断无用类的标准:

    该类的所有的实例都已经回收,也就是java堆中不存在该类的任何实例。

    加载该类的ClassLoader已经回收。

    该类对应的java.lang.class 对象没有在任何地方被引用,在无法再任何地方通过反射访问到该类的方法。

    虚拟机可以对满足上述标准的无用类进行回收。

    三、Hotspot如何发起内存回收

    https://www.cnblogs.com/jing99/p/6071808.html

    四、垃圾收集算法:

    1.标记清除算法

     最基础的算法是标记清除算法.标记清除算法分为两个阶段,标记和清除阶段。首先,先标记处来所有需要回收的对象,在都是标记完成后统一回收所有被标记的对象。之所以说他是最基础的算法,那是因为后续的收集算法都是基于这种思路并对其不足而进行改进的。他的不足主要有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间

    2.复制算法

       为了解决效率问题,一种被称为复制的收集算法就出现了,他将可用内存按容量划分为大小相同的两块,每一次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面去,然后再把已使用的内存空间进行一次清理。这样使得每次都是对整个半区进行内存回收,内存分配时候也就不用考虑内存碎片的问题等复杂的情况。

    缺点:将内存缩小为原来的一半,内存变小。如果不想浪费50%的空间,就需要额外的空间进行担保,以对象内存中的对象100%存活这种极端情况。

    优点:不用考虑空间碎片的问题。

     

    3.标记整理算法

     根据老年代的特点,提出了标记整理算法。

    标记过程和标记清除过程一样,但后续步骤不是直接对可回收对象进行直接清理,而是让所有存活对象像一段移动,然后直接清理掉端边界以外的内存。

     

    4.分代收集

    根据对象不同的存活周期将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每天都有大量的对象死去,只有少量存活,那就采用复制算法。在老年代中存活率比较高,没有额外的空间对他进行担保,就必须使用标记清理或者标记整理算法来对他进行收集。

     

    五、垃圾收集器

       如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。由于java虚拟机规范对垃圾收集器实现没有任何的规范因此不同的厂商,不同的版本的虚拟机所提供的垃圾收集器都有可能会有很大的区别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

    虚拟机中所包含的垃圾收集器如下图:

    连线代代表他们可以组合使用。下面分别对以上垃圾收集器进行说明:

    01)Serial 是历史悠久的收集器,在垃圾回收期间或中断用户线程,是一个单线程的收集器,在进行垃圾收集的时候暂停其他所有的工作线程,直至他收集结束。由于它是在用户不可见的时候暂停线程,这对许多用户来说都是不可接受的。适合于单个CPU,单线程的情况下面,如果在桌面运行程序下面 即Client模式下面虚拟机来说是一个很好的选择,因为停顿时间很小。是新生代收集器

    Serial 收集器的运行过程

    02)ParNew 收集器

      Parnew收集器其实是serial收集器的多线程版本,除了可以使用多线程进行垃圾收集以外,其余行为包括Serial收集器的可用的所有控制参数。Parnew收集器的运行过程示例图:

     

    需要注意的是除了Serial收集器以外,只有ParNew收集器才能与CMS收集器一起工作。ParNew在单核CPU的情况下面绝对不会有比Serial收集器更好的效果,甚至由于存在线程的开销,该收集器在通过超线程技术实现的两个CPU的环境汇都不能百分百的超越serial收集器。当然随着CPU数量的增加,他对于GC时系统资源的有效利用还是很有好处的。

    两种概念的解释:

    并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能是交替执行),用户线程在继续运行,而垃圾收集程序运行于另一个CPU上面。

    并行:指多条垃圾收集线程并行执行,但此时用户线程处于等待状态。

    03)  Parallel Scavenge收集器

    Paraller Scavenge收集器是一个新生代收集器,他也是采用复制算法的收集器,又是并行的多线程收集器。他与ParNew收集器最大的不同是Parallel Scavenge收集器要达到一个可控的吞吐量。吞吐量= 运行用户代码的时间/(运行用户代码时间+垃圾收集器时间)。如:虚拟机共运行了100分钟,垃圾收集用了1分钟,那么吞吐量是99%。停顿时间越短,用户的体验就会更好。高的吞吐量可以高效率的利用CPU的时间,尽快的完成程序的运算任务,主要适合在后台不需要交互的任务。

     Parallel Scavenge 收集器提供了两个参数用于精确的控制吞吐量,分别是最大垃圾收集停顿时间的-XX:MaxGcPauseMills 参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。MaxGcPauseMills参数是一个大于0的毫秒数,收集器将尽量地保证内存回收花费的时间不超过设定值。如果把停顿时间调小,会导致GC次数频繁,吞吐量会下降。

    GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总数的时间的比率,相当于是吞吐量的倒数。因此Paraller Scavenge收集器也别成为吞吐量优先的收集器。Paraller Scavenge收集器还有一个参数-XX:+UseAdapterSizePolicy值得关注,这是一个自适应策略的参数,一旦打开了以后,就不需要尽进行手动的指定新生代大小,Eden和Survivor区域的比例等细节参数了。虚拟机会根据当前的系统信息动态的调整这些参数,成为GC自适应的调节策略。

    04)Serial Old收集器

    Serial Old收集器是老年代版本,他同样是一个单线程收集器,这个收集器的主要意义是主要是Client模式下面的虚拟机使用。Serial Old收集器使用的是标记整理算法。用途是:在JDK1.5的版本之前与Paraller Scavenge 收集器搭配使用,另一种用途是CMS收集器的备选方案。

    05) Parallel Old 收集器是Paraller Scavenge 收集器的老年大版本,使用多线程和标记整理算法。因为新生代收集器Parallel Scavenge 收集器无法与CMS收集器一起工作,所以如果Parallel Scavenge 收集器选择了在新生代使用,那么老年代只能使用Parallel  Scavenge收集器。Parallel Old 收集器只能和Parallel Scavenge收集器一起工作搭配。Serial和 ParNew收集器无法与Parallel Old收集器一起工作。

    06)CMS 收集器

    CMS 收集器是一个以获取最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网网站或者B/S系统的服务端上,这类应用尤为重视服务的响应速度,希望停顿时间最短,已给用户最好的体验。CMS收集器就非常符合这类应用的需求。

    CMS收集器是基于标记清除算法实现的,它的运作过程相对于前面的几种的收集器来说更复杂一些。整个过程分为初始标记、并发标记、重新标记、并发清除。其中初始标记和重新标记这两个步骤任然需要“stop the  world“。初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快.并发标记阶段就是进行GC Root Tracing的过程,而重现标记阶段则是为了修正并发标记期间因用户程序运作而导致的那一部分对象的记录,这个阶段的停顿时间一般会比初始标记的时间稍长一些,但远比并发标记时间短。由于整个过程中耗时最长的并发标记和并发清除收集线程都是可以与用户线程一起工作,所以,从总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以清楚的看出CMS收集器的运行过程和需要停顿的时间:

    CMS是一款优秀的收集器,它的主要优点在名字上面已经体现出来了:并发收集、低停顿,sun公司的一些官方文档中也称之为并发低停顿收集器。但是CMS收集器还远达不到完美的程度,他有以下三个明显的缺点:

     1.CMS收集器对CPU资源特别敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,他虽然不会导致用户线程的停顿,但是会为了占用了一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数量是(CPU数量+3)/4 ,也就是当CPU在4个以上的时候,并发回收垃圾收集线程不少于25%的CPU资源,并随着CPU的数量的增加而下降。但是当CPU不足4个的时候,比如说两个的时候CMS收集线程对用户的影响就会很大,如果本来负载就比较大的时候,还分出一部分去执行垃圾收集线程,就可能导致用户线程执行变慢,这让人很难接受。为了解决这一个问题,虚拟机提供了一种增量式并发收集器,思想是:在并发标记、清理的时候让GC线程、用户线程交替执行,尽量减少GC线程独占资源的时间,这样整个垃圾收集的过程会变得更长,但是对用户程序的影响就会小一些,也就是速度下降没有那么明显。

    2.CMS收集器无法处理浮动垃圾。由于CMS并发清理阶段是在和用户线程一起执行的,伴随着程序的运行自然就还会新的垃圾产生,这一部分垃圾在出现在标记过程之后的话,CMS无法在当次收集过程中进行处理,只好在下一次的垃圾清理的时候在进行清理,这一部分垃圾成为浮动垃圾。

    3.CMS垃圾收集器是基于的标记清除算法,收集结束后会有大量的空间碎片产生。空间碎片过多的时候,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很多的剩余,但是无法找到足够大的连续的空间来分配当前的对象,不得不提前触发一次Full  GC 。为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住的时候,进行FullGC时候进行内存碎片的合并过程,内存整理过程是无法并发的,空间碎片问题没有了,但是停顿时间会变得很长。为此虚拟机提供了另一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩FullGC,来执行一次带压缩的(默认为0,表示每次full Gc 都进行内存整理)

    07) G1收集器

    G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予他的使命是(在比较长的时间)未来可替换jdk1.5的CMS收集器。与其他收集器相比,G1收集器的特点:

    并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-the-world停顿的时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以用并发的方式让java线程继续运行。

    分代收集:与其他收集器一样,分代概念在G1收集器中得已保留。可以采用不同的处理方式去处理新创建的对象和已经存活了一段时间的对象。

    空间整合:G1收集器是基于标记整理算法实现的垃圾收集器,不会产生空间碎片。

    可预测性停顿:这是G1相对于CMS的另一个优势。G1除了追求低停顿以外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集收集上的时间不超过N毫秒,这个几乎是已经是实时java(RTSJ)的垃圾收集器的特征了。

    在G1之前的其他收集都是在老年代和新生代之间进行来及回收的,而G1不在是这样。使用G1收集器的话,java的堆内存布局就与其他的收集器有很大的差别,通它将整个java堆划分为多个大小相等的独立区域,虽然还保留着老年代和新生代的概念,但是新生代和老年代不在是物理隔离了,他们都是一部分Region集合。

    G1收集器之所以能建立起来可预测的停顿时间模型,是因为它可以有计划的避免在整个java堆中进行全区域的垃圾回收。G1跟踪各个region里面的垃圾堆价值大小,在后台维护一个优先列表,每次都根据允许的收集时间,优先回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

       在G1收集器中,Region之间的对象引用以及其他收集器的新生代与老年代之间的对象引用,虚拟机都是采用Rememberd Set 来避免全局扫描的。G1中每一个Region都有一个与之对应的Remember Set,虚拟机发现程序在对Reference 类型的数据进行写操作的时候,会产生一个Write Barrier 暂时中断写操作,检查Reference 引用的对象是否处于不同的Region中(在分代收集的时候,是检查是否老年代中的对象引用了新生代的对象),如果是,便通过CardTable 把相关的引用信息记录到被引用对象的所属的Region的Remember Set之中。当进行垃圾回收的时候,把GC根节点的枚举范围中加入Rememeber Set即可保证不对全局扫描也不会有遗漏。

  • 相关阅读:
    P4178 Tree
    CF437D The Child and Zoo
    CF1032G Chattering ST表+倍增
    P4165 [SCOI2007]组队 推柿子+差分
    P1450 [HAOI2008]硬币购物 容斥原理+完全背包
    P6275 [USACO20OPEN]Sprinklers 2: Return of the Alfalfa P 轮廓线DP
    P6009 [USACO20JAN]Non-Decreasing Subsequences P 矩阵优化DP
    P2605 [ZJOI2010]基站选址 线段树优化DP
    P5597 【XR-4】复读 思维题 +二叉树合并
    P5304 [GXOI/GZOI2019]旅行者 最短路+位运算优化
  • 原文地址:https://www.cnblogs.com/histlyb/p/7900233.html
Copyright © 2011-2022 走看看