要想搞懂啊垃圾回收机制,首先就要知道垃圾回收主要回收的是哪些数据,这些数据主要在哪一块区域。java
Java8和Java8以前的相同点有不少。算法
都有虚拟机栈,本地方法栈,程序计数器,这三个是线程隔离的也称是线程独有的;数组
本地内存和堆是线程共享的。安全
Java8和以前JVM内存区域不一样的是,Java8中增长了元空间,取消了永久代,Java8以前永久代是在堆中的,而以后方法区搬到了元空间中,元空间存在于本地内存中。多线程
下面详细说一下各个内存区域的特色。并发
XX:MaxPermSize
参数指定上限,因此若是动态生成类信息或者大量执行String.intern方法(直接将字符串放入永久代)就会形成永久代内存溢出引发OOM。所以在Java8中就将方法区的实现移动到本地内存中的元空间中,这样方法区就不受JVM的控制了,也就不进行GC,所以有必定的性能提高,一样这样方法区也方便在元空间中进行统一管理。引用计数法就是每一个对象引用你一次,你的对象头上就+1,若是没有对象引用你(引用次数为0),那你凉凉,等着被回收吧。框架
听着引用计数确实能够解决咱们没法识别哪些对象该被回收的问题,可是他还有个主要问题没被解决,那就是循环引用。什么是循环引用呢?jvm
例如性能
A a = new Instance("a"); B b = new Instance("b"); a.instance=b; b.instance=a; a=null; b=null;
虽然到最后a和b两个对象都被置为null,可是由于他们以前都互相引用过,因此引用的次数都是1,所以没法被回收。因此现代虚拟机都不使用这种方法来判断对象是否该回收了。线程
现代虚拟机主要是采用这种算法进行判断独享是否是该被回收。它的原理是从一个叫作GC Root对象为起点出发,引出他们指向的下一个节点,再从下一个节点出发,继续引出下一个,以此类推。这样就经过GCRoot节点串成了一条引用链,若是相关对象不是这个引用链上的节点,则会被断定为垃圾,而后会被回收。
可达性分析算法能够解决上述循环引用的问题,由于两个对象a,b都没有在GC Root所在的引用链上。
对象最后一次垂死挣扎的机会,finalize
方法。
当发生GC时,finalize方法给对象一个催死挣扎的机会,当对象可回收的时候,首先会判断这个对象是否是执行了finalize
方法,若是未执行,则会先执行finalize
,咱们能够在finalize方法内部将本对象和GC Root关联起来,这样执行完方法后,GC会再次判断对象是否可被回收,若是可达则不会进行回收。
finalize方法只会执行一次,若是第一次执行方法这个对象变成了可达确实不会回收可是再次对这个对象进行回收的时候,则会忽略finalize方法。
哪些对象能够做为GC Root呢?
JDK1.2后,Java对引用的概念进行了补充,将引用分为强引用,软引用,弱引用,虚引用。强度依次递减。
上面讲了如何经过可达性分析算法来是被哪些数据是垃圾,那具体该经过什么方式回收垃圾呢?
垃圾回收算法主要由如下几种方式
先用可达性分析算法标记处可回收的对象。
对可回收对象进行回收。
操做简单不须要移动数据,可是缺点也很明显,就是存在内存碎片。若是想要再申请的内存空间大小大于碎片的大小就会申请失败,那要是将回收过的内存区域和原先没有数据的区域都合并到一块就能够了。
将堆等分红两块内存区域,咱们暂且把他记做区域A和区域B,A负责分配对象,区域B不分配,A区域中的对象标记为可回收时,将A中全部不可回收的对象都赶到B中,对A进行统一清除,B中存活的对象紧邻排列。
这种算法的缺陷也很明显,我明明堆中还有不少空余的空间可是不能分配,只能使用一半的空间,另外每次回收都要移动对象,这是很浪费资源而且效率低下。
标记整理法与标记清除法不一样的是他多了一步整理内存碎片的操做。将全部存活对象都往一端移动,紧邻排列,再清除另外一端的全部区域,这样就解决了内存碎片的问题。
可是还有缺点:每次清除可回收对象都要进行对象的移动,效率很低下。
分代收集算法整合了上面所讲的全部算法,综合以上算法优势,最大程度避免他们的缺点,所以使现代虚拟机采用的首选算法,于其说他是算法,倒不是说它是一种收集策略。
通过有关专家研究代表,大部分对象(98%)都是朝生夕死,通过一次年轻代的GC就会被回收,因此分代收集算法是根据对象存活周期的不一样将堆分红新生代和老年代,在Java8以前还有永久代,新生代和老年代的比例是1:2,新生代又分为Eden区,from Survivor区,to Survivor区,简称S0区和S1区,Eden:S0:S1=8:1:1,咱们将新生代发生的GC叫作Young GC或Minor GC,将老年代发生的GC叫作 Old GC也叫Full GC。
新生代的分配和回收
新生代对象通常在Eden区分配,当Eden满的时候,会发生一次Minor GC,此次Minor GC不多有对象存活,由于大部分对象都是朝生夕死的,少部分存活的对象会被移动到S0区,同时这些对象的年龄+1,最后将Eden区中的全部对象都清除,释放空间。(复制算法
)
当发生下一次Minor GC时,会把Eden区中存活的对象和S0中存活的对象都移动到S1,这些对象的年龄+1,同时清空Eden和S0空间。
若再次发生MinorGC重复上面的步骤,只不过此次是将Eden和S1中存活的对象移动到S1,每次Young GC都是S0和S1来回之间移动。由于S0和S1区域比较小,因此下降复制算法频繁拷贝带来的开销。
对象是如何进入到老年代的
大对象直接进入老年代
大对象通常指的是很长的字符串或者数组,当出现大对象时,会致使提早触发GC,虚拟机提供了一个-XX:PertenureSizeThreshold
参数若是对象大小大于这个参数设置的阈值,就认为是大对象,直接分配到老年代,这样作的目的是避免Eden和S1,S0区域之间发生大对象的拷贝。
长期存活的对象进入老年代
虚拟机给每一个对象都定义了一个年龄计数器,每次通过Minor GC后还存活下来的对象,他们的年龄+1,当计数器的值加到必定程度(默认是15),就会晋升到老年代,对象晋升老年代的阈值能够经过参数-XX:MaxTenuringThershold
设置。
动态对象年龄断定
这种状况也会晋升到老年代,若是Survivor区中相同年龄的对象大小之和大于Survivor区空间大小的一半,这时候年龄大于等于该年龄的对象也会直接进入老年代,无需和MaxTrnuringThershold参数进行比较。
空间分配担保
在发生Minor GC以前,虚拟机会检查老年代最大可用的连续空间是否大于新生代全部对象的总空间,若是大于,那么就能够确保Minor GC是安全的,若是不大于,虚拟机会查看HandlePromotionFailure
设置值是否容许担保失败,若是容许的话,那么会继续检查老年代对象的平均大小,若是大于则进行GC,否者可能进行一次Full GC。尽管空间分配担保绕的圈子很大,可是平时仍是会开启担保的,由于能够减小Full GC的频率。
Stop The World
若是老年代满了,会触发Full GC,Full GC会同时回收新生代和老年代,也就是对整个堆进行GC,他会致使Stop The World,形成很大的性能开销。Stop The World就是指在这个GC期间,除了垃圾回收线程在工做,其余线程会被挂起。
通常Full GC会致使工做线程停顿时间过长,若是再次期间,服务端收到了客户端不少的请求,则会被拒绝服务,因此才要尽可能减小Full GC的次数。
所以虚拟机设计成新生代分为Eden,S0,S1,而且设置对象年龄阈值,默认新生代和老年代的比例是1:2都是为了不对象过早的进入老年代,尽量晚的触发Full GC。
老年代采用标记整理法进行垃圾回收。
由于GC都会影响性能,因此咱们要在一个合适的时间点发起GC,这个时间点被称为安全点(Safe Point),这个时间点的选定既不能太少让GC时间太长,也不能过于频繁以致于过度的增大运行时的负荷,安全点通常是如下特定的位置:
收集算法实际上是理论层面的,垃圾收集器才是这些理论具体的实现。
Serial收集器收集的是新生代,单线程的垃圾收集器,单线程意味着他只会使用一个CPU或者一个收集线程来进行垃圾回收,他在进行垃圾回收的时候,其余用户线程会暂停,在GC期间这个应用不可用。可是在用户端模式下,他是简单有效的,对于限定单个CPU的环境来讲,Serial单线程模式无需与其余线程进行交互,较少了开销,专心作GC能将单线程的优点发挥到极致,在桌面应用场景下,通常不会给虚拟机分配很大的内存,所以STW(Stop The World)的时间会在100ms之内,这点停顿是能够接受的,因此对于Client模式下的虚拟机,Serial收集器是新生代的默认收集器。
ParNew收集器是Serial收集器的多线程版本,除了使用多线程,其余收集算法以及对象分配,回收策略都和Serial同样。ParNew主要工做在服务端,服务端若是接受的请求多了,响应时间就很重要,多线程可让垃圾与回收更快,也就是减小了STW时间,提高响应速度,因此许多运行在服务端的虚拟机采用的新生代垃圾收集器是ParNew ,还有一点,他只能和CMS收集器配合工做,CMS是一个彻底并发的收集器,第一次实现了垃圾收集线程和用户线程同时工做,采用的是传统的GC收集器代码框架,与Serial,ParNew共用一套代码框架,因此能够和这两个收集器配合工做。
在多CPU状况下,ParNew收集器垃圾收集更快,能够有效减小STW时间,提高服务端响应速度。
Parallel Scavenge收集器也是一个使用复制算法,多线程,工做在新生代的垃圾收集器。看起来他的功能和ParNew收集器同样。可是还有一些不一样。
关注点不一样:CMS等垃圾收集器关注的是尽量缩短垃圾收集时用户线程停顿的时间,而Parallel Scavenge目标是达到一个可控制的吞吐量。
$$
吞吐量=用户代码运行时间/(用户代码运行时间+垃圾收集时间)
$$
CMS等垃圾收集器更适合用于与用户交互的应用,提高用户体验。而Parallel Scavenge收集器关注的是吞吐量,因此更适合用于后台运算等不须要太多用户交互的任务。
Parallel Scavenge收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾手机时间的-XX:MaxGCPauseMillis
以及设置吞吐量大小的-XX:GCtimeRatio
默认是99%。
除了这两个参数外,还有第三个参数-XX:UseAdaptiveSizePolicy
开启这个参数后,就不要手工指定新生代大小比例等细节,只须要设置好堆的大小,以及最大垃圾收集时间和吞吐量,虚拟机就会根据当前系统运行状况动态调整这些参数尽量的达到设定的最大垃圾收集时间和吞吐量,自适应策略是ParallelScavenger和ParNew的重要区别。
Serial收集器是工做在新生代的单线程收集器。Serial Old是工做在老年代的单线程收集器。这个收集器的主要意义是给Client模式下的虚拟机使用,若是在Server模式下,他还有两大用途,一种是和JDK1.5以及以前的版本的Parallel Scavenge收集器配合使用,另外一种是做为CMS的备用方案。
Parallel Old收集器是相对于Parallel Scavenge收集器的老年代版本,使用多线程和标记整理法。
CMS收集器是以实现最短STW时间为目标的收集器,若是应艳红很重视服务的相应速度,但愿给用户最好的体验,则CMS收集器是不错的选择。
CMS虽然工做在老年代可是回收算法使用的是标记清除法。
一、初始标记
二、并发标记
三、从新标记
四、并发清除
在这四个步骤中,初始标记和从新标记两个阶段会发生STW,形成用户线程挂起,不过初始标记仅仅标记GC Root可以关联的对象,速度很快,从新标记是进行GC Root跟踪引用链的过程,是为了修正并发标记期间由于用户线程继续运行而致使标记产生变更的哪一部分对象的标记记录,这一阶段停顿时间通常比初始标记更长,但比并发标记短。
整个过程执行时间最长的是并发标记和标记整理,不过这两个阶段用户线程均可以工做,因此不影响应用的正常使用,因此整体上看,能够认为CMS是内存回收线程和用户线程一块儿并发执行的。
可是他有三个缺点:
-XX:CMSInitiatingOccupancyFraction
来设置,可是若是设置过高容易致使CMS运行期间预留的内存不够,致使Concurrent Model Failure,这时会启用Serial Old收集器进行老年代的收集工做,可是Serial old 是单线程的,这就致使STW时间更长了。-XX:+UseCMSCompactAtFullCollection
,这个参数是当CMS顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理会致使STW,停顿时间会变长,还能够用另外一个参数-XX:CMSFullGCsBeforeCompation
用来设置执行多少次不压缩的Full GC事后再进行一次压缩。G1收集器欧式面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器。
特色以下:
与CMS相比,它有如下方面表现得更为出色。
他为何能创建可预测模型呢?
主要缘由是他和传统的内存分配存储方式不同。传统内存分配是连续的,新生代,老年代。可是G1的存储地址不是连续的,每一代都是用N个不连续的大小相同的Region,每一个Region占有一块连续的虚拟内存地址。和传统相比还多了一个H区,表明Humongous,标会存储的是大对象。当对象大小大于Region的通常,就直接分给老年代,防止GC时反复拷贝大对象。
这样作G1就能够根据Region的价值大小(回收所得到的空间大小以及回收经验值)进行排序,维护成一个优先级列表,根据容许的时间,回收截止最大的Region,也就避免了整个老年代的回收,减小了STW形成的停顿时间。
G1收集器工做步骤
筛选阶段会根据各个Region的回收价值和成本进行排序,根据用户指望的GC停顿时间来制定回收计划。