原创不易,如需转载,请注明出处http://www.javashuo.com/article/p-oljsybpz-hc.html,多多支持哈!html
GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会致使程序或系统的不稳定甚至崩溃,Java提供的GC功能能够自动监测对象是否超过做用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操做方法。Java程序员不用担忧内存管理,由于垃圾收集器会自动进行管理。要请求垃圾收集,能够调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc()。java
哪些内存须要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象。那么如何找到这些对象?git
那么问题又来了,如何选取GCRoots对象呢?在Java语言中,能够做为GCRoots的对象包括下面几种:程序员
下面给出一个GCRoots的例子,以下图,为GCRoots的引用链,obj八、obj九、obj10都没有到GCRoots对象的引用链,因此会进行回收。github
对于可达性分析算法而言,未到达的对象并不是是“非死不可”的,若要宣判一个对象死亡,至少须要经历两次标记阶段。算法
对F-Queue中对象进行第二次标记,若是对象在finalize方法中拯救了本身,即关联上了GCRoots引用链,如把this关键字赋值给其余变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,若是对象仍是没有拯救本身,那就会被回收。以下代码演示了一个对象如何在finalize方法中拯救了本身,然而,它只能拯救本身一次,第二次就被回收了。具体代码以下:编程
public class GC {segmentfault
public static GC SAVE_HOOK = null; public static void main(String[] args) throws InterruptedException { // 新建对象,由于SAVE_HOOK指向这个对象,对象此时的状态是(reachable,unfinalized) SAVE_HOOK = new GC(); //将SAVE_HOOK设置成null,此时刚才建立的对象就不可达了,由于没有句柄再指向它了,对象此时状态是(unreachable,unfinalized) SAVE_HOOK = null; //强制系统执行垃圾回收,系统发现刚才建立的对象处于unreachable状态,并检测到这个对象的类覆盖了finalize方法,所以把这个对象放入F-Queue队列,由低优先级线程执行它的finalize方法,此时对象的状态变成(unreachable, finalizable)或者是(finalizer-reachable,finalizable) System.gc(); // sleep,目的是给低优先级线程从F-Queue队列取出对象并执行其finalize方法提供机会。在执行完对象的finalize方法中的super.finalize()时,对象的状态变成(unreachable,finalized)状态,但接下来在finalize方法中又执行了SAVE_HOOK = this;这句话,又有句柄指向这个对象了,对象又可达了。所以对象的状态又变成了(reachable, finalized)状态。 Thread.sleep(500); // 这里楼主说对象处于(reachable,finalized)状态应该是合理的。对象的finalized方法被执行了,所以是finalized状态。又由于在finalize方法是执行了SAVE_HOOK=this这句话,原本是unreachable的对象,又变成reachable了。 if (null != SAVE_HOOK) { //此时对象应该处于(reachable, finalized)状态 // 这句话会输出,注意对象由unreachable,通过finalize复活了。 System.out.println("Yes , I am still alive"); } else { System.out.println("No , I am dead"); } // 再一次将SAVE_HOOK放空,此时刚才复活的对象,状态变成(unreachable,finalized) SAVE_HOOK = null; // 再一次强制系统回收垃圾,此时系统发现对象不可达,虽然覆盖了finalize方法,但已经执行过了,所以直接回收。 System.gc(); // 为系统回收垃圾提供机会 Thread.sleep(500); if (null != SAVE_HOOK) { // 这句话不会输出,由于对象已经完全消失了。 System.out.println("Yes , I am still alive"); } else { System.out.println("No , I am dead"); } } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("execute method finalize()"); // 这句话让对象的状态由unreachable变成reachable,就是对象复活 SAVE_HOOK = this; }
}数组
运行结果以下:多线程
leesf null finalize method executed! leesf yes, i am still alive :) no, i am dead : (
由结果可知,该对象拯救了本身一次,第二次没有拯救成功,由于对象的finalize方法最多被虚拟机调用一次。此外,从结果咱们能够得知,一个堆对象的this(放在局部变量表中的第一项)引用会永远存在,在方法体内能够将this引用赋值给其余变量,这样堆中对象就能够被其余变量所引用,即不会被回收。
一、方法区的垃圾回收主要回收两部份内容:
二、既然进行垃圾回收,就须要判断哪些是废弃常量,哪些是无用的类?
如何判断无用的类呢?须要知足如下三个条件:
这是最基础的算法,标记-清除算法就如同它的名字样,分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,标记完成后统一回收全部被标记的对象。这种算法的不足主要体如今效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会致使之后程序运行过程当中在须要分配较大对象时,没法找到足够的连续内存而不得不提早触发一次垃圾收集动做。标记-清除算法执行过程如图:
复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,而后再把已经使用过的内存空间一次性清理掉。这样每次只须要对整个半区进行内存回收,内存分配时也不须要考虑内存碎片等复杂状况,只须要移动指针,按照顺序分配便可。复制算法的执行过程如图:
不过这种算法有个缺点,内存缩小为了原来的一半,这样代价过高了。如今的商用虚拟机都采用这种算法来回收新生代,不过研究代表1:1的比例很是不科学,所以新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。固然,咱们没有办法保证每次回收都只有很少于10%的对象存活,当Survivor空间不够用时,须要依赖老年代进行分配担保(Handle Promotion)。
复制算法在对象存活率较高的场景下要进行大量的复制操做,效率很低。万一对象100%存活,那么须要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,所以通常不能直接选用复制算法。根据老年代的特色,有人提出了另一种标记-整理算法,过程与标记-清除算法同样,不过不是直接对可回收对象进行清理,而是让全部存活对象都向一端移动,而后直接清理掉边界之外的内存。标记-整理算法的工做过程如图:
垃圾收集器就是上面讲的理论知识的具体实现了。不一样虚拟机所提供的垃圾收集器可能会有很大差异,咱们使用的是HotSpot,HotSpot这个虚拟机所包含的全部收集器如图:
上图展现了7种做用于不一样分代的收集器,若是两个收集器之间存在连线,那说明它们能够搭配使用。虚拟机所处的区域说明它是属于新生代收集器仍是老年代收集器。多说一句,咱们必须明确一个观点:没有最好的垃圾收集器,更加没有万能的收集器,只能选择对具体应用最合适的收集器。这也是HotSpot为何要实现这么多收集器的缘由。OK,下面一个一个看一下收集器。
最基本、发展历史最久的收集器,这个收集器是一个采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工做,另外一方面也意味着它进行垃圾收集时必须暂停其余线程的全部工做,直到它收集结束为止。后者意味着,在用户不可见的状况下要把用户正常工做的线程所有停掉,这对不少应用是难以接受的。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,由于它简单而高效。用户桌面应用场景中,分配给虚拟机管理的内存通常来讲不会很大,收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿是彻底能够接受的。Serial收集器运行过程以下图所示:
说明:1. 须要STW(Stop The World),停顿时间长。2. 简单高效,对于单个CPU环境而言,Serial收集器因为没有线程交互开销,能够获取最高的单线程收集效率。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其他行为和Serial收集器彻底同样,包括使用的也是复制算法。ParNew收集器除了多线程之外和Serial收集器并无太多创新的地方,可是它倒是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的缘由是,除了Serial收集器外,目前只有它能与CMS收集器配合工做(看图)。CMS收集器是一款几乎能够认为有划时代意义的垃圾收集器,由于它第一次实现了让垃圾收集线程与用户线程基本上同时工做。ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至因为线程交互的开销,该收集器在两个CPU的环境中都不能百分之百保证能够超越Serial收集器。固然,随着可用CPU数量的增长,它对于GC时系统资源的有效利用仍是颇有好处的。它默认开启的收集线程数与CPU数量相同,在CPU数量很是多的状况下,可使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。ParNew收集器运行过程以下图所示:
Parallel Scavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器,可是它的特色是它的关注点和其余收集器不一样。介绍这个收集器主要仍是介绍吞吐量的概念。CMS等收集器的关注点是尽量缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是打到一个可控制的吞吐量。所谓吞吐量的意思就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总运行100分钟,垃圾收集1分钟,那吞吐量就是99%。另外,<font color=red>Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器</font>。
停顿时间短适合须要与用户交互的程序,良好的响应速度能提高用户体验;高吞吐量则能够高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不须要太多交互的任务。
虚拟机提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要觉得前者越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。因为与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先收集器”。<font color=red>Parallel Scavenge收集器有一个-XX:+UseAdaptiveSizePolicy参数,这是一个开关参数,这个参数打开以后,就不须要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行状况以及性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。</font>若是对于垃圾收集器运做原理不太了解,以致于在优化比较困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。
Serial收集器的老年代版本,一样是一个单线程收集器,使用“标记-整理算法”,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK 1.6以后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,均可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。运行过程以下图所示:
<font color=red>CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器</font>。使用标记 - 清除算法,收集过程分为以下四步:
其中,并发标记与并发清除两个阶段耗时最长,可是能够与用户线程并发执行。运行过程以下图所示:
说明:
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停全部应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分红不少区域,G1收集器经过将对象从一个区域复制到另一个区域,完成了清理工做。这就意味着,在正常的处理过程当中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 若是一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,可是若是它是一个短时间存在的巨型对象,就会对垃圾收集器形成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。若是一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1主要有如下特色:
在G1以前的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1再也不是这样。使用G1收集器时,<font color=red>Java堆的内存布局与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分(能够不连续)Region的集合</font>。
<font color="red">注意:这些space必须是地址连续的空间</font>
对象分配
优先在Eden区分配
在JVM内存模型一文中, 咱们大体了解了VM年轻代堆内存能够划分为一块Eden区和两块Survivor区. 在大多数状况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另外一块Survivor区域, 若是在Minor GC期间发现新生代存活对象没法放入空闲的Survivor区, 则会经过空间分配担保机制使对象提早进入老年代(空间分配担保见下).
大对象直接进入老年代
Serial和ParNew两款收集器提供了-XX:PretenureSizeThreshold的参数, 令大于该值的大对象直接在老年代分配, 这样作的目的是避免在Eden区和Survivor区之间产生大量的内存复制(大对象通常指 须要大量连续内存的Java对象, 如很长的字符串和数组), 所以大对象容易致使还有很多空闲内存就提早触发GC以获取足够的连续空间. 然而取历次晋升的对象的平均大小也是有必定风险的, 若是某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能致使担保失败(Handle Promotion Failure, 老年代也没法存放这些对象了), 此时就只好在失败后从新发起一次Full GC(让老年代腾出更多空间).
空间分配担保
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 因为新生代使用复制收集算法, 为了提高内存利用率, 只使用了其中一个Survivor做为轮换备份, 所以当出现大量对象在Minor GC后仍然存活的状况时, 就须要老年代进行分配担保, 让Survivor没法容纳的对象直接进入老年代, 但前提是老年代须要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是没法明确知道的, 所以Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 若是条件成立, 则进行Minor GC, 不然进行Full GC(让老年代腾出更多空间).
对象晋升
年龄阈值
VM为每一个对象定义了一个对象年龄(Age)计数器, 对象在Eden出生若是经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 之后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增长到必定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.
提早晋升: 动态年龄断定
然而VM并不老是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代: 若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就能够直接进入老年代, 而无须等到晋升年龄.
发生在年轻代的GC算法,通常对象(除了巨型对象)都是在eden region中分配内存,当全部eden region被耗尽没法申请内存时,就会触发一次young gc,这种触发机制和以前的young gc差很少,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。
当愈来愈多的对象晋升到老年代old region时,为了不堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并非一个old gc,除了回收整个young region,还会回收一部分的old region,这里须要注意:是一部分老年代,而不是所有老年代,能够选择哪些old region进行收集,从而能够对垃圾回收的耗时时间进行控制。
若是对象内存分配速度过快,mixed gc来不及回收,致使老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会致使异常长时间的暂停时间,须要进行不断的调优,尽量的避免full gc.
首先查看你使用的垃圾回收器是什么?
java -XX:+PrintCommandLineFlags -version
我的博客地址:
csdn: https://blog.csdn.net/tiantuo6513cnblogs:https://www.cnblogs.com/baixianlong
segmentfault:https://segmentfault.com/u/baixianlong
本文参考:
<font color="gray">https://www.cnblogs.com/xiaox...;/font>
<font color="gray">https://zhuanlan.zhihu.com/p/...;/font>