由浅入深了解GC原理

GCGarbage Collection)很大程度上帮助Java程序员解决了内存释放的问题,有了GC,就不须要再手动的去控制内存的释放。程序员

在阅读以前须要了解的相关概念:算法

Java 堆内存分为新生代和老年代,新生代中又分为 1Eden 区域 和 2Survivor 区域。

1、什么是GC(Garbage Collection)

GC垃圾收集,Java提供的GC能够自动监测对象是否超过做用域从而达到自动回收内存的目的。segmentfault

每一个程序员都遇到过内存溢出的状况,程序运行时,内存空间是有限的,那么如何及时的把再也不使用的对象清除将内存释放出来,这就是GC要作的事。数组

须要GC的内存区域数据结构

JVM 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出作入栈和出栈操做,实现了自动的内存清理,所以,咱们的内存垃圾回收主要集中于 JAVA 堆和方法区中,在程序运行期间,这部份内存的分配和使用都是动态的。多线程

注意:
对于 Java8HotSpots 取消了永久代,那么是否是也就没有方法区了呢?固然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不一样的?存储位置不一样,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;存储内容不一样,元空间存储类的元信息,静态变量和常量池等并入堆中。至关于永久代的数据被分到了堆和元空间中。

GC的对象并发

当一个对象到GC Roots不可达时,在下一个垃圾回收周期中尝试回收该对象,若是对象重写了finalize(),并在这个方法中成功自救(将自身赋予某个引用),那么这个对象不会被回收。但若是这个对象没有重写finalize()方法或已执行过这个方法,该对象将会被回收。函数

须要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活经常使用的有两种办法:引用计数算法和可达性分析算法。布局

  • 引用计数算法:

每一个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时能够回收。此方法简单,没法解决对象相互循环引用的问题。性能

  • 可达性分析算法(Reachability Analysis):

GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的,不可达对象。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象;
  • 方法区中类静态属性实体引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI引用的对象。

何时触发GC

  • 程序调用System.gc时,但不是必然执行
  • 系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并中止应用线程)

GC又分为 Minor GCFull GC (也称为 Major GC)
Minor GC触发条件:当Eden区满时,触发Minor GC
Full GC触发条件:

  • 调用System.gc时,系统建议执行Full GC,可是没必要然执行
  • 老年代空间不足
  • 方法去空间不足
  • 经过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

GC作了什么事

主要作了清理对象,整理内存的工做。Java堆分为新生代和老年代,采用了不一样的回收方式。

GC经常使用算法

GC经常使用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法

目前主流的JVMHotSpot)采用的是分代收集算法。

标记-清除算法(Mark-Sweep)

首先标记出全部须要回收的对象,标记完成后回收全部被标记的对象。不足主要体如今效率和空间,从效率的角度讲,标记和清除效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会致使须要分配较大对象时,没法找到足够的连续内存而提早触发一次垃圾收集动做。

从堆栈和静态存储区出发,遍历全部的引用,进而找出全部存活的对象,若是活着,就标记。只有所有标记完毕的时候,清理动做才开始。在清理的时候,没有标记的对象将会被释放,不会发生任何动做。可是剩下的堆空间是不连续的,垃圾回收器要是但愿获得连续空间的话,就得从新整理剩下的对象。

优势:标记—清除算法中每一个活着的对象的引用只须要找到一个便可,找到一个就能够判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。

缺点:它的缺点就是效率比较低(递归与全堆对象遍历)。每一个活着的对象都要在标记阶段遍历一遍;全部对象都要在清除阶段扫描一遍,所以算法复杂度较高。没有移动对象,致使可能出现不少碎片空间没法利用的状况。
image.png

标记-压缩算法(标记-整理)(Mark-Compact)

过程与标记-清除算法同样,不过不是直接对可回收对象进行清理,而是让全部存活对象都向一端移动,而后直接清理掉边界之外的内存。在标记阶段,该算法也将全部对象标记为存活和死亡两种状态;不一样的是,在第二个阶段,该算法并无直接对死亡的对象进行清理,而是将全部存活的对象整理一下,放到另外一处空间,而后把剩下的全部对象所有清除。这样就达到了标记-整理的目的。

优势:该算法不会像标记-清除算法那样产生大量的碎片空间。

缺点:若是存活的对象过多,整理阶段将会执行较多复制操做,致使算法效率下降。
image.png

复制(Copying)算法

将可用内存分为两块,每次只用其中一块,当一块内存用完了,就将还存活的对象复制到另一块上,而后再把已经使用过的内存空间一次性清理掉,循环下去。这样每次只需对整个半区进行内存回收,内存分配时也不须要考虑内存碎片等复杂状况,只须要移动指针,按照顺序分配便可。

优势:实现简单;不产生内存碎片

缺点:内存缩小为原来的一半,代价过高

如今商用虚拟机都采用这种算法来回收新生代,不过 1:1的比例很是不科学,所以新生代的内存被划分为一块较大的 Eden空间和两块较小的 Survivor空间,每次使用 Eden和其中一块 Survivor。每次回收时,将 EdenSurvivor中还存活着的对象一次性复制到另一块 Survivor空间上,最后清理掉 Eden和刚才用过的 Survivor空间。 HotSpot虚拟机默认 Eden区和 Survivor区的比例为 8:1,意思是每次新生代中可用内存空间为整个新生代容量的 90%。固然,咱们没法保证每次回收都少于 10%的对象存活,当 Survivor空间不够用时,须要依赖老年代进行分配担保( Handle Promotion)。
image.png

分代收集(Generational Collection)算法

分代收集算法根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenur)。在新生代中,因为对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,因此可使用标记-整理或者标记-清除

新生代(Young)分为Eden区,From区与To区:
image.png

当系统建立一个对象的时候,老是在Eden区操做,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。通常来讲这时候并非全部的对象都没用了,因此就会把还能用的对象复制到From区:
image.png

这样整个Eden区就被清理干净了,能够继续建立新的对象,当Eden区再次被用完,就再触发一次YoungGC,而后注意,这个时候跟刚才稍稍有点区别。此次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区:
image.png

再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区:
image.png

通过若干次YoungGC后,有些对象在FromTo之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是尚未被回收,就会被复制到老年代:
image.png

老年代通过这么几回折腾,也就扛不住了(空间被用完),那就来次集体大扫除(Full GC),也就是全量回收。若是Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。因此要合理设置年轻代与老年代的大小,尽可能减小Full GC的操做。

垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器,可是可能会产生较长的停顿,只使用一个线程去回收。
启用命令:-XX:+UseSerialGC

Parallel收集器

并行GC的好处是提高垃圾回收的性能,减小串行回收带来的问题,也有停顿,但能够并行回收,一边标记对象一边执行线程,总体上提高了回收的性能。
启用命令:
-XX:+UseParallelGC

  • 使用Parallel收集器 + 老年代串行

-XX:+UseParallelOldGC

  • 使用Parallel收集器 + 老年代并行

image.png

CMS收集器

CMS收集器是以获取最短回收停顿时间为目标的收集器,基于”标记-清除”(Mark-Sweep)算法实现,整个过程分为四个步骤:

  • 初始标记 (Stop the World事件CPU停顿很短) ,仅标记GC Roots能直接关联到的对象,速度快;
  • 并发标记 (收集垃圾跟用户线程一块儿执行) ,初始标记和从新标记仍须要 Stop the World,并发标记过程就是进行 GC Roots Tracing的过程;
  • 从新标记 (Stop the World事件CPU停顿,比初始标记稍长,远比并发标记短),修正并发标记期因用户程序继续运做而致使标记产生变更的那部分对象的标记记录,这个阶段停顿时间比初始标记阶段稍长些,比并发标记时间短;
  • 并发清理-清除算法。

整个过程当中最耗时的并发标记和并发清除过程,收集器线程均可与用户线程一块儿工做,整体上来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。

CMS收集器优势:并发收集,低停顿

CMS收集器缺点:

  • CMS收集器对CPU资源很是敏感
  • CMS处理器没法处理浮动垃圾
  • CMS基于“标记--清除”算法实现,会产生大量空间碎片,会在大对象分配时提早触发Full GC。为解决这个问题,CMS提供了一个开关参数,用于在CMS要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程没法并发,停顿时间变长;

CMS也提供了整理碎片的参数:

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理

  • 整理过程是独占的,会引发停顿时间变长

-XX:+CMSFullGCsBeforeCompaction

  • 设置进行几回Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads

  • 设定CMS的线程数量(通常状况约等于可用CPU数量)

CMS的提出是想改善GC的停顿时间,在GC过程当中的确作到了减小GC时间,可是一样致使产生大量内存碎片,又须要消耗大量时间去整理碎片,从本质上并无改善时间。  

G1(Garbage First)收集器

G1是一款面向服务端应用的垃圾收集器。与CMS收集器相比G1收集器有如下特色:

  • 空间整合:G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会由于没法找到连续空间而提早触发下一次GC
  • 可预测停顿:这是G1的另外一大优点,下降停顿时间是G1CMS的共同关注点,但G1除了追求低停顿外,还能创建可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已是实时JavaRTSJ)的垃圾收集器的特征了。
  • 并行于并发:充分使用多个CPU来缩短Stop the World停顿时间。
  • 分代收集:采用不一样方式处理新建立的对象和已存活一段时间,熬过屡次GC的旧对象,以获取更好的收集效果。

使用G1收集器时,Java堆的内存布局与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔阂了,它们都是一部分(能够不连续)Region的集合。

G1运做步骤:

  • 初始标记(Initial-Mark)(Stop the World事件CPU停顿只处理垃圾);

这个阶段是停顿的(Stop the World Event),而且会触发一次普通Mintor GC
对应GC log:GC pause (young) (inital-mark)

  • Root Region Scanning;

程序运行过程当中会回收survivor区(存活到老年代),这一过程必须在young GC以前完成。

  • 并发标记(Concurrent Marking)(与用户线程并发执行);

在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的全部对象都是垃圾,那个这个区域会被当即回收。同时,并发标记过程当中,会计算每一个区域的对象活性(区域中存活对象的比例)。

  • 最终标记(Stop the World事件CPU停顿处理垃圾);

此阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

  • 筛选回收(Stop the World事件根据用户指望的GC停顿时间回收);

多线程清除失活对象,会有Stop the World事件。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

finalize()方法

finalize的做用

  • finalize()Objectprotected方法,子类能够覆盖该方法以实现资源清理工做,GC在回收对象以前调用该方法;
  • finalize()C++中的析构函数不是对应的。C++中的析构函数调用的时机是肯定的(对象离开做用域或delete掉),但Java中的finalize的调用具备不肯定性;
  • 不建议用finalize方法完成“非内存资源”的清理工做,但建议用于:

① 清理本地对象(经过JNI建立的对象);
② 做为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其余资源释放方法。

finalize的问题

  • 一些与finalize相关的方法,因为一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法;
  • System.gc()System.runFinalization()方法增长了finalize方法执行的机会,但不可盲目依赖它们;
  • Java语言规范并不保证finalize方法会被及时地执行、并且根本不会保证它们会被执行;
  • finalize方法可能会带来性能问题。由于JVM一般在单独的低优先级线程中完成finalize的执行;
  • 对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的;
  • finalize方法至多由GC执行一次(用户固然能够手动调用对象的finalize方法,但并不影响GCfinalize的行为)。

finalize的执行过程(生命周期)

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。不然,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,不然,对象“复活”。

具体的finalize流程:

对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义以下:

  • unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,由于该对象是可达的。
  • finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC经过F-Queue队列和一专用线程完成finalize的执行。
  • finalized: 表示GC已经对该对象执行过finalize方法。
  • reachable: 表示GC Roots引用可达。
  • finalizer-reachable(f-reachable):表示不是reachable,但可经过某个finalizable对象可达。
  • unreachable:对象不可经过上面两种途径可达。

状态变迁图:
image.png

状态变迁说明:

  • 新建对象首先处于[reachable, unfinalized]状态(A);
  • 随着程序的运行,一些引用关系会消失,致使状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态;
  • JVM检测处处于unfinalized状态的对象变成f-reachableunreachableJVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H);
  • 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。因为是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(KJ)。该动做将影响某些其余对象从f-reachable状态从新回到reachable状态(L, M, N);
  • 处于finalizable状态的对象不能同时是unreahable的,由上一点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,导致其变成reachable。这也是图中只有八个状态点的缘由;
  • 程序员手动调用finalize方法并不会影响到上述内部标记的变化,所以JVM只会至多调用finalize一次,即便该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为;
  • JVM检测到finalized状态的对象变成unreachable,回收其内存(I);
  • 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)。

注:System.runFinalizersOnExit()等方法可使对象即便处于reachable状态,JVM仍对其执行finalize方法。

总结

GC垃圾收集,Java提供的GC能够自动监测对象是否超过做用域从而达到自动回收内存的目的。

判断一个对象是否存活经常使用的有两种办法:引用计数算法和可达性分析算法。

GC经常使用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法

无论选择哪一种GC算法,Stop the World都是不可避免的。Stop the World意味着从应用中停下来并进入到GC执行过程当中去。一旦Stop the World发生,除了GC所需的线程外,其余线程都将中止工做,中断了的线程直到GC任务结束才继续它们的任务。GC调优一般就是为了改善Stop the World的时间。

关于程序设计的几点建议:

  • 尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为 null.咱们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们通常效率较低。若是程序容许,尽早将不用的引用对象赋为 null,这样能够加速GC的工做。
  • 尽可能少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。可是,它会加大GC的工做量,所以尽可能少采用finalize方式回收资源。
  • 若是须要使用常用的图片,可使用soft应用类型。它能够尽量将图片保存在内存中,供程序调用,而不引发OutOfMemoryException
  • 注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来讲,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量每每容易引发悬挂对象(dangling reference),形成内存浪费。
  • 当程序有必定的等待时间,程序员能够手动执行System.gc(),通知GC运行,可是Java语言规范并不保证GC必定会执行。使用增量式GC能够缩短Java程序的暂停时间。
本文由博客一文多发平台 OpenWrite 发布!
更多内容请点击个人博客 沐晨
相关文章
相关标签/搜索