垃圾收集 Garbage Collection 一般被称为“GC”,本文详细讲述Java垃圾回收机制。html
导读:java
一、什么是GC程序员
二、GC经常使用算法算法
三、垃圾收集器数组
四、finalize()方法详解性能优化
五、总结--根据GC原理来优化代码数据结构
正式阅读以前须要了解相关概念:多线程
Java 堆内存分为新生代和老年代,新生代中又分为1个 Eden 区域 和 2个 Survivor 区域。架构
每一个程序员都遇到过内存溢出的状况,程序运行时,内存空间是有限的,那么如何及时的把再也不使用的对象清除将内存释放出来,这就是GC要作的事。并发
一、须要GC的内存区域
jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出作入栈和出栈操做,实现了自动的内存清理,所以,咱们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部份内存的分配和使用都是动态的。
二、GC的对象
须要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活经常使用的有两种办法:引用计数和可达分析。
(1)引用计数:每一个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时能够回收。此方法简单,没法解决对象相互循环引用的问题。
(2)可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括:
虚拟机栈中引用的对象。
方法区中类静态属性实体引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI引用的对象。
三、何时触发GC
(1)程序调用System.gc时能够触发
(2)系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并中止应用线程)
GC又分为 minor GC 和 Full GC (也称为 Major GC )
Minor GC触发条件:当Eden区满时,触发Minor GC。
a.调用System.gc时,系统建议执行Full GC,可是没必要然执行
b.老年代空间不足
c.方法去空间不足
d.经过Minor GC后进入老年代的平均大小大于老年代的可用内存
e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
四、GC作了什么事
主要作了清理对象,整理内存的工做。Java堆分为新生代和老年代,采用了不一样的回收方式。(回收方式即回收算法详见后文)
2、GC经常使用算法
GC经常使用算法有: 标记-清除算法 , 标记-压缩算法 , 复制算法 , 分代收集算法。
目前主流的JVM(HotSpot)采用的是分代收集算法。
一、标记-清除算法
为每一个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每一个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操做。
优势
最大的优势是,标记—清除算法中每一个活着的对象的引用只须要找到一个便可,找到一个就能够判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。
缺点
它的缺点就是效率比较低(递归与全堆对象遍历)。每一个活着的对象都要在标记阶段遍历一遍;全部对象都要在清除阶段扫描一遍,所以算法复杂度较高。没有移动对象,致使可能出现不少碎片空间没法利用的状况。
标记-压缩法是标记-清除法的一个改进版。一样,在标记阶段,该算法也将全部对象标记为存活和死亡两种状态;不一样的是,在第二个阶段,该算法并无直接对死亡的对象进行清理,而是将全部存活的对象整理一下,放到另外一处空间,而后把剩下的全部对象所有清除。这样就达到了标记-整理的目的。
优势
该算法不会像标记-清除算法那样产生大量的碎片空间。
缺点
若是存活的对象过多,整理阶段将会执行较多复制操做,致使算法效率下降。
图例
左边是标记阶段,右边是整理以后的状态。能够看到,该算法不会产生大量碎片内存空间。
该算法将内存平均分红两部分,而后每次只使用其中的一部分,当这部份内存满的时候,将内存中全部存活的对象复制到另外一个内存中,而后将以前的内存清空,只使用这部份内存,循环下去。
注意:
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将全部存活的对象复制到另外一个区域内。
实现简单;不产生内存碎片
缺点
每次运行,总有一半内存是空的,致使可以使用的内存空间只有原来的一半。
图例
如今的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为 新生代(Young)和老年代(Tenure) 。在新生代中,因为对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,因此可使用标记-整理 或者 标记-清除。
具体过程: 新生代(Young)分为 Eden区,From区与To区
当系统建立一个对象的时候,老是在Eden区操做,当这个区满了,那么就会触发一次 YoungGC ,也就是 年轻代的垃圾回收 。通常来讲这时候不是全部的对象都没用了,因此就会把还能用的对象复制到From区。
这样整个Eden区就被清理干净了,能够继续建立新的对象,当Eden区再次被用完,就再触发一次YoungGC,而后呢,注意,这个时候跟刚才稍稍有点区别。此次触发YoungGC后, 会将Eden区与From区还在被使用的对象复制到To区 ,
再下一次YoungGC的时候,则是将 Eden区与To区中的还在被使用的对象复制到From区 。
通过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到如今还没挂掉,对不起,一块儿滚到(复制)老年代吧。
老年代通过这么几回折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除( Full GC),也就是全量回收。若是Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。因此要合理设置年轻代与老年代的大小,尽可能减小Full GC的操做。
3、垃圾收集器
若是说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
串行收集器是最古老,最稳定以及效率高的收集器
可能会产生较长的停顿,只使用一个线程去回收
-XX:+UseSerialGC
-XX:+UseParNewGC(new表明新生代,因此适用于新生代)
Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,须要多核支持
-XX:ParallelGCThreads 限制线程数量
相似ParNew
新生代复制算法
老年代标记-压缩
更加关注吞吐量
-XX:+UseParallelGC
-XX:+UseParallelOldGC
2.3 其余GC参数
-XX:MaxGCPauseMills
-XX:GCTimeRatio
这两个参数是矛盾的。由于停顿时间和吞吐量不可能同时调优
CMS运行过程比较复杂,着重实现了标记的过程,可分为
1. 初始标记(会产生全局停顿)
2. 并发标记(和用户线程一块儿)
3. 从新标记 (会产生全局停顿)
4. 并发清除(和用户线程一块儿)
这里就能很明显的看出,为何CMS要使用标记清除而不是标记压缩,若是使用标记压缩,须要多对象的内存位置进行改变,这样程序就很难继续执行。可是标记清除会产生大量内存碎片,不利于内存分配。
CMS收集器特色:
尽量下降停顿
会影响系统总体吞吐量和性能
清理不完全
由于和用户线程一块儿运行,不能在空间快满时再清理(由于也许在并发GC的期间,用户线程又申请了大量内存,致使内存不够)
一旦 concurrent mode failure产生,将使用串行收集器做为后备。
CMS也提供了整理碎片的参数:
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理
-XX:+CMSFullGCsBeforeCompaction
-XX:ParallelCMSThreads
CMS的提出是想改善GC的停顿时间,在GC过程当中的确作到了减小GC时间,可是一样致使产生大量内存碎片,又须要消耗大量时间去整理碎片,从本质上并无改善时间。
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是将来能够替换掉JDK1.5中发布的CMS收集器。
与CMS收集器相比G1收集器有如下特色:
(1) 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会由于没法找到连续空间而提早触发下一次GC。
(2)可预测停顿,这是G1的另外一大优点,下降停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能创建可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已是实时Java(RTSJ)的垃圾收集器的特征了。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔阂了,它们都是一部分(能够不连续)Region的集合。
G1的新生代收集跟ParNew相似,当新生代占用达到必定比例的时候,开始出发收集。
和CMS相似,G1收集器收集老年代对象会有短暂停顿。
步骤:
(1)标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),而且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
(2)Root Region Scanning,程序运行过程当中会回收survivor区(存活到老年代),这一过程必须在young GC以前完成。
(3)Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的全部对象都是垃圾,那个这个区域会被当即回收(图中打X)。同时,并发标记过程当中,会计算每一个区域的对象活性(区域中存活对象的比例)。
(4)Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
(5)Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
(6)复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
4、finalize()方法详解
1. finalize的做用
(1)finalize()是Object的protected方法,子类能够覆盖该方法以实现资源清理工做,GC在回收对象以前调用该方法。
(2)finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是肯定的(对象离开做用域或delete掉),但Java中的finalize的调用具备不肯定性
(3)不建议用finalize方法完成“非内存资源”的清理工做,但建议用于:① 清理本地对象(经过JNI建立的对象);② 做为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其余资源释放方法。其缘由可见下文[finalize的问题]
2. finalize的问题
(1)一些与finalize相关的方法,因为一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法
(2)System.gc()与System.runFinalization()方法增长了finalize方法执行的机会,但不可盲目依赖它们
(3)Java语言规范并不保证finalize方法会被及时地执行、并且根本不会保证它们会被执行
(4)finalize方法可能会带来性能问题。由于JVM一般在单独的低优先级线程中完成finalize的执行
(5)对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
(6)finalize方法至多由GC执行一次(用户固然能够手动调用对象的finalize方法,但并不影响GC对finalize的行为)
3. finalize的执行过程(生命周期)
(1) 首先,大体描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。不然,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,不然,对象“复活”。
(2) 具体的finalize流程:
对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义以下:
状态变迁图:
变迁说明:
(1)新建对象首先处于[reachable, unfinalized]状态(A)
(2)随着程序的运行,一些引用关系会消失,致使状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态
(3)若JVM检测处处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。
(4)在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。因为是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动做将影响某些其余对象从f-reachable状态从新回到reachable状态(L, M, N)
(5)处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,导致其变成reachable。这也是图中只有八个状态点的缘由
(6)程序员手动调用finalize方法并不会影响到上述内部标记的变化,所以JVM只会至多调用finalize一次,即便该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
(7)若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)
(8)若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)
(9)注:System.runFinalizersOnExit()等方法可使对象即便处于reachable状态,JVM仍对其执行finalize方法
在此我向你们推荐一个架构学习交流群。交流学习群号:821169538 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。
根据GC的工做原理,咱们能够经过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。一些关于程序设计的几点建议:
1.最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为 null.咱们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们通常效率较低。若是程序容许,尽早将不用的引用对象赋为null.这样能够加速GC的工做。
2.尽可能少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。可是,它会加大GC的工做量,所以尽可能少采用finalize方式回收资源。
3.若是须要使用常用的图片,可使用soft应用类型。它能够尽量将图片保存在内存中,供程序调用,而不引发OutOfMemory.
4.注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来讲,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量每每容易引发悬挂对象(dangling reference),形成内存浪费。
5.当程序有必定的等待时间,程序员能够手动执行System.gc(),通知GC运行,可是Java语言规范并不保证GC必定会执行。使用增量式GC能够缩短Java程序的暂停时间。