在1960年诞生于MIT的Lisp语言首次使用了动态内存分配和垃圾收集技术,能够实现垃圾回收的一个基本要求是语言是类型安全的,如今使用的包括Java、Perl、ML等。java
一、当须要排查各类内存溢出、内存泄漏问题时;
程序员
二、当垃圾收集成为系统达到更高并发量的瓶颈时;算法
咱们就须要对这些"自动化"技术实话必要的监控和调节;
数组
一、哪些内存须要回收?即如何判断对象已经死亡;缓存
二、何时回收?即GC发生在何时?须要了解GC策略,与垃圾回收器实现有关;安全
三、如何回收?即须要了解垃圾回收算法,及算法的实现--垃圾回收器bash
下面先来了解两种判断对象再也不被引用的算法,再来谈谈对象的引用,最后来看如何真正宣告一个对象死亡。服务器
(A)、很难解决对象之间相互循环引用的问题数据结构
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;复制代码
当两个对象再也不被访问时,由于相互引用对方,致使引用计数不为0;
多线程
更复杂的循环数据结构,如图:
(B)、而且开销较大,频繁且大量的引用变化,带来大量的额外运算;
主流的JVM都没有选用引用计数算法来管理内存;
当一个对象到GC Roots没有任何引用链相连时(从GC Roots到这个对象不可达),则证实该对象是不可用的;
(1)虚拟机栈(栈帧中本地变量表)中引用的对象;
(2)方法区中类静态属性引用的对象;
(3)方法区中常量引用的对象;
(4)本地方法栈中JNI(Native方法)引用的对象;
主要在执行上下文中和全局性的引用;
三、优势后面会针对HotSpot虚拟机实现的可达性分析算法进行介绍,看看是它如何解决这些缺点的。
JVM规范规定reference类型来表示对某个对象的引用,能够想象成相似于一个指向对象的指针;
对象的操做、传递和检查都经过引用它的reference类型的数据进行操做;若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用;这种定义太过狭隘,没法描述更多信息;
(ii)、JDK1.2后,对引用概念进行了扩充,将引用分为:(1)强引用(Strong Reference)
程序代码广泛存在的,相似"Object obj=new Object()";
只要强引用还存在,GC永远不会回收被引用的对象;(2)软引用(Soft Reference)
用来描述还有用但并不是必需的对象;
直到内存空间不够时(抛出OutOfMemoryError以前),才会被垃圾回收;最经常使用于实现对内存敏感的缓存;SoftReference类实现;
(3)弱引用(Weak Reference)
用来描述非必需对象;只能生存到下一次垃圾回收以前,不管内存是否足够;WeakReference类实现;
(4)虚引用(Phantom Reference)
也称为幽灵引用或幻影引用;彻底不会对其生存时间构成影响;惟一目的就是能在这个对象被回收时收到一个系统通知;PhantomRenference类实现;
(A)没有必要执行
没有必要执行的状况:
(1) 对象没有覆盖finalize()方法;
(2) finalize()方法已经被JVM调用过;
这两种状况就能够认为对象已死,能够回收;
(B) 有必要执行
对有必要执行finalize()方法的对象,被放入F-Queue队列中;
稍后在JVM自动创建、低优先级的Finalizer线程(可能多个线程)中触发这个方法;
二、第二次标记GC将对F-Queue队列中的对象进行第二次小规模标记;
finalize()方法是对象逃脱死亡的最后一次机会:
(A)、若是对象在其finalize()方法中从新与引用链上任何一个对象创建关联,第二次标记时会将其移出"即将回收"的集合;
(B)、若是对象没有,也能够认为对象已死,能够回收了;
一个对象的finalize()方法只会被系统自动调用一次,通过finalize()方法逃脱死亡的对象,第二次不会再调用;
finalize()是Object类的一个方法,是Java刚诞生时为了使C/C++程序员容易接受它所作出的一个妥协,但不要看成相似C/C++的析构函数;
由于它执行的时间不肯定,甚至是否被执行也不肯定(Java程序的不正常退出),并且运行代价高昂,没法保证各个对象的调用顺序(甚至有不一样线程中调用);一、充当"安全网"
当显式的终止方法没有调用时,在finalize()方法中发现后发出警告;
但要考虑是否值得付出这样的代价;如FileInputStream、FileOutputStream、Timer和Connection类中都有这种应用;
二、与对象的本地对等体有关
本地对等体:普通对象调用本地方法(JNI)委托的本地对象;本地对等体不会被GC回收;
若是本地对等体不拥有关键资源,finalize()方法里能够回收它(如C/C++中malloc(),须要调用free());
若是有关键资源,必须显式的终止方法;
通常状况下,应尽可能避免使用它,甚至能够忘掉它。前面对可达性分析算法进行介绍,并看到了它在判断对象存活与死亡的做用,下面看看是HotSpot虚拟机是如何实现可达性分析算法,如何解决相关缺点的。
一、消耗大量时间
从前面可达性分析知道,GC Roots主要在全局性的引用(常量或静态属性)和执行上下文中(栈帧中的本地变量表);是JVM在后台自动发起和自动完成的;在用户不可见的状况下,把用户正常的工做线程所有停掉;
在类加载时,计算对象内什么偏移量上是什么类型的数据;
在JIT编译时,也会记录栈和寄存器中的哪些位置是引用;
这样GC扫描时就能够直接得知这些信息;
运行中,很是多的指令都会致使引用关系变化;若是为这些指令都生成对应的OopMap,须要的空间成本过高;
问题解决:只在特定的位置记录OopMap引用关系,这些位置称为安全点(Safepoint);
即程序执行时并不是全部地方都能停顿下来开始GC;
二、安全点的选定只有具备这些功能的指令才会产生Safepoint;
三、如何在安全点上停顿(A)抢先式中断(Preemptive Suspension)
不须要线程主动配合,实现以下:
(1)在GC发生时,首先中断全部线程;
(2)若是发现不在Safepoint上的线程,就恢复让其运行到Safepoint上;如今几乎没有JVM实现采用这种方式;
(B)主动式中断(Voluntary Suspension)
(1)在GC发生时,不直接操做线程中断,而是仅简单设置一个标志;
(2)让各线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起;
而轮询标志的地方和Safepoint是重合的;
在JIT执行方式下:test指令是HotSpot生成的轮询指令;一条test汇编指令便完成Safepoint轮询和触发线程中断;
这就须要安全区域来解决;程序不执行时没有CPU时间(Sleep或Blocked状态),没法运行到Safepoint上再中断挂起;
(1)线程执行进入Safe Region,首先标识本身已经进入Safe Region;
(2)线程被唤醒离开Safe Region时,其须要检查系统是否已经完成根节点枚举(或整个GC);
若是已经完成,就继续执行;不然必须等待,直到收到能够安全离开Safe Region的信号通知,这样就不会影响标记结果;虽然HotSpot虚拟机中采用了这些方法来解决对象可达性分析的问题,但只是大大减小了这些问题影响,并不能彻底解决,如GC停顿"Stop The World"是垃圾回收重点关注的问题,后面介绍垃圾回收器时应注意:低GC停顿是其一个关注。
下面先来了解Java虚拟机垃圾回收的几种常见算法:标记-清除算法、复制算法、标记-整理算法、分代收集算法、火车算法,介绍它们的算法思路,有什么优势和缺点,以及主要应用场景。
(A)标记
首先标记出全部须要回收的对象;标记过程以下
(1)第一次标记在可达性分析后发现对象到GC Roots没有任何引用链相连时,被第一次标记;
而且进行一次筛选:此对象是否必要执行finalize()方法;
对有必要执行finalize()方法的对象,被放入F-Queue队列中;
(2)第二次标记
GC将对F-Queue队列中的对象进行第二次小规模标记;
在其finalize()方法中从新与引用链上任何一个对象创建关联,第二次标记时会将其移出"即将回收"的集合;
对第一次被标记,且第二次还被标记(若是须要,但没有移出"即将回收"的集合),就能够认为对象已死,能够进行回收。
(B)清除
两次标记后,还在"即将回收"集合的对象将被统一回收;
执行过程以下图:
(A)效率问题
标记和清除两个过程的效率都不高;
(B)空间问题
标记清除后会产生大量不连续的内存碎片;这会致使分配大内存对象时,没法找到足够的连续内存;从而须要提早触发另外一次垃圾收集动做;
四、应用场景针对老年代的CMS收集器;
执行过程以下图:
三、缺点
(A)空间浪费
可用内存缩减为原来的一半,太过浪费(解决:能够改良,不按1:1比例划分);
(B)效率随对象存活率升高而变低
当对象存活率较高时,须要进行较多复制操做,效率将会变低(解决:后面的标记-整理算法);
四、应用场景(A)弱代理论
分代垃圾收集基于弱代理论(weak generational hypothesis),具体描述以下:
(1)大多数分配了内存的对象并不会存活太长时间,在处于年轻代时就会死掉;
(2)不多有对象会从老年代变成年轻代;
其中IBM研究代表:新生代中98%的对象都是"朝生夕死";
因此并不须要按1:1比例来划份内存(解决了缺点1);
(B)HotSpot虚拟机新生代内存布局及算法
(1)将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;默认Eden:Survivor=8:1,即每次可使用90%的空间,只有一块Survivor的空间被浪费;
(C)分配担保(1)标记
标记过程与"标记-清除"算法同样;
(2)整理
但后续不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动;
而后直接清理掉端边界之外的内存;
执行过程以下图:
(A)不会像复制算法,效率随对象存活率升高而变低
老年代特色:
对象存活率高,没有额外的空间能够分配担保;
因此老年代通常不能直接选用复制算法算法;
而选用标记-整理算法;
(B)不会像标记-清除算法,产生内存碎片
由于清除前,进行了整理,存活对象都集中到空间一侧;
3 缺点如Serial Old收集器、G1(从总体看);
(A)新生代
每次垃圾收集都有大批对象死去,只有少许存活;因此可采用复制算法;
(B)老年代
对象存活率高,没有额外的空间能够分配担保;使用"标记-清理"或"标记-整理"算法;
结合上面对新生代的内存划分介绍和上篇文章对Java堆的介绍,能够得出HotSpot虚拟机通常的年代内存划分,以下图:
能够根据各个年代的特色采用最适当的收集算法;
三、缺点仍然不能控制每次垃圾收集的时间;
四、应用场景如HotSpot虚拟机中所有垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留);
(1)选择标号最小的火车;
(2)若是火车的记忆集合是空的, 释放整列火车并终止, 不然进行第三步操做;
(3)选择火车中标号最小的车箱;
(4)对于车箱记忆集合的每一个元素:
若是它是一个被根引用引用的对象, 那么, 将拷贝到一列新的火车中去;
若是是一个被其它火车的对象指向的对象, 那么, 将它拷贝到这个指向它的火车中去.;
假设有一些对象已经被保留下来了, 那么经过这些对象能够触及到的对象将会被拷贝到同一列火车中去;
若是一个对象被来自多个火车的对象引用, 那么它能够被拷贝到任意一个火车去;
这个步骤中, 有必要对受影响的引用集合进行相应地更新;
(5)、释放车箱而且终止;
收集过程会删除一些空车厢和空车,当须要的时候也会建立一些车厢和火车。
执行过程以下图:
垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现,不一样商家、不一样版本的JVM所提供的垃圾收集器可能会有很在差异,下面主要介绍HotSpot虚拟机中的垃圾收集器。
JDK7/8后,HotSpot虚拟机全部收集器及组合(连线),以下图:
Serial/Serial Old组合收集器运行示意图以下:
ParNew/Serial Old组合收集器运行示意图以下:
(A)有一些特色与ParNew收集器类似
新生代收集器;
采用复制算法;
多线程收集;
(B)主要特色是:它的关注点与其余收集器不一样
CMS等收集器的关注点是尽量地缩短垃圾收集时用户线程的停顿时间;
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
关于吞吐量与收集器关注点说明详见本节后面;
上面介绍的都是新生代收集器,接下来开始介绍老年代收集器;
Serial Old是 Serial收集器的老年代版本;
一、特色
针对老年代;
采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
单线程收集;
Serial/Serial Old收集器运行示意图以下:
Parallel Scavenge/Parallel Old收集器运行示意图以下:
整个过程当中耗时最长的并发标记和并发清除均可以与用户线程一块儿工做;
CMS收集器运行示意图以下:
CMS收集器3个明显的缺点
(A)对CPU资源很是敏感
(B)没法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
(C)产生大量内存碎片
一、特色
(A)并行与并发
(C)结合多种垃圾收集算法,空间整合,不产生碎片
从总体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
这是一种相似火车算法的实现;都不会产生内存碎片,有利于长时间运行;
(D)可预测的停顿:低停顿的同时实现高吞吐量
G1除了追求低停顿处,还能创建可预测的停顿时间模型;
能够明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
不计算维护Remembered Set的操做,能够分为4个步骤(与CMS较为类似)。
(A)初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中建立新对象;须要"Stop The World",但速度很快;
(B)并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;耗时较长,但应用程序也在运行;并不能保证能够标记出全部的存活对象;
(C)最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运做而致使标记变更的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;
须要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提高效率;
(D)筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本;
而后根据用户指望的GC停顿时间来制定回收计划;
最后按计划回收一些价值高的Region中垃圾对象;
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另外一个空的Region,而且在此过程当中压缩和释放内存;
能够并发进行,下降停顿时间,并增长吞吐量;
少数状况下,可能直接分配在老年代中。
分配的细节取决于当前使用哪一种垃圾收集器组合,以及JVM中内存相关参数设置。
接下来将会讲解几条最广泛的内存分配规则。
默认Eden:Survivor=8:1,即每次可使用90%的空间,只有一块Survivor的空间被浪费;
大多数状况下,对象在新生代Eden区中分配;
当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);
Minor GC时,若是发现存活的对象没法所有放入Survivor空间,只好经过分配担保机制提早转移到老年代。
常常出现大对象容易致使内存还有很多空间就提早触发GC,以获取足够的连续空间来存放它们,因此应该尽可能避免使用建立大对象;
JVM给每一个对象定义一个对象年龄计数器,其计算流程以下:
在Eden中分配的对象,经Minor GC后还存活,就复制移动到Survivor区,年龄为1;
然后每经一次Minor GC后还存活,在Survivor区复制移动一次,年龄就增长1岁;
若是年龄达到必定程度,就晋升到老年代中;
若是在Survivor空间中相同年龄的全部对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就能够直接进入老年代
(1)该类全部实例都已经被回收(即Java椎中不存在该类的任何实例);
(2)加载该类的ClassLoader已经被回收,也即经过引导程序加载器加载的类不能被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法;
一、CGLib在Spring、Hibernate等框架中对类进行加强时会使用;
二、VM的动态语言也会动态建立类来实现语言的动态性;
三、另外,JSP(第一次使用编译为Java类)、基于OSGi频繁自定义ClassLoader的应用(同一个类文件,不一样加载器加载视为不一样类)等;
从OS请求空间,而后分红块;
类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);
当为类加载器卸载类时,它的块被回收再使用或返回到操做系统;
元数据使用由mmap分配的空间,而不是由malloc分配的空间;
三、相关参数下面介绍的是一些思路,并不是是具体的参数设置。
(1)停顿时间
GC停顿时间越短就适合须要与用户交互的程序,良好的响应速度能提高用户体验;
与用户交互较多的场景,以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用;
(2)吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
高吞吐量能够高效率地利用CPU时间,尽快完成运算的任务,主要适合在后台计算而不须要太多交互的任务;
应用程序运行在具备多个CPU上,对暂停时间没有特别高的要求;
程序主要在后台进行计算,而不须要与用户进行太多交互;
例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;
(3)覆盖区(Footprint)
在达到前面两个目标的状况下,尽可能减小堆的内存空间,以得到更好的空间局部性;
能够减小到不知足前两个目标为止,而后再解决未知足的目标;
若是是动态收缩的堆设置,堆的大小将随着垃圾收集器试图知足竞争目标而振荡;
总结就是:低停顿、高吞吐量、少用内存资源;
通常这些目标都相互影响的,增大堆内存得到高吞吐量但会增加停顿时间,反之亦然,有时需折中处理。
通常都会先根据平台性能来选择好垃圾收集器,以及设置好其参数;
在运行中,一些收集器还会收集监控信息来自动地、动态的调整垃圾回收策略;
因此当咱们不知道何如选择收集器和调整时,应该首先让JVM自适应调整;
若是不能知足,或者经过打印设置的参数信息,发现能够有更好的调优时,能够进行手动指定参数进行设置,并测试;
没有最好的收集器,更没有万能的收集;
选择的只能是对具体应用最适合的收集器;
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
到实践调优阶段,那必需要了解每一个具体收集器的行为特色、优点和劣势、调节参数等(请参考前面的文章内容);而后根据明确指望的目标,选择具体应用最适合的收集器;
当选择使用某种并行垃圾收集器时,应该指按期望的具体目标而不是指定堆的大小;
让垃圾收集器自动地、动态的调整堆的大小来知足指望的行为;
即堆的大小将随着垃圾收集器试图知足竞争目标而振荡;
固然有时发现问题,堆的大小、划分也是须要进行一些调整的,通常规则:除非应用程序没法接受长时间的暂停,不然能够将堆调的尽量大一些;
除非发现问题的缘由在于老年代的垃圾收集或应用程序暂停次数过多,不然你应该将堆的较大部分分给年轻代;
等等…
例如,使用Parallel Scavenge/Parallel Old组合,这是一种值得推荐的方式:
一、只需设置好内存数据大小(如"-Xmx"设置最大堆);
二、而后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;
三、那些具体细节参数的调节就由JVM自适应完成;
设置调整后,应该经过在产生环境下进行不断测试,来分析是否达到咱们的目标;
引用: