“两者之间存在一堵由内存分配与GC技术筑建起来的高墙,墙里面的人想出去,墙外面的人却想进来”。
理解GC机制,必须要对JAVA内存区域很熟悉,如果不熟悉JAVA内存区域,建议先看上一篇文章《Java内存区域》
JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。
在Java语言中,GC Roots包括:
虚拟机栈帧中引用的对象(方法内部引用外部的对象)。
方法区中类静态属性实体引用的对象(依赖)。
方法区中常量引用的对象(对象的属性依赖)。
本地方法栈中JNI引用的对象。
JDK1.2之后,为了方便进行可达性判断与提高GC性能,对引用进行分类。
引用类型 | 回收时机 | sample |
---|---|---|
强引用 | 只要强引用还在,就永远不会回收 | A a=new A(); |
软引用 | 如果引用不在,立即回收,否则,在发生OOM之前,对软引用对象进行回收,SorfReference实现软引用 | … |
弱引用 | 下一次GC之前将被回收(ThreadLocal的内存泄漏问题,K是软引,而V不是,如果线程是线程池,那么线程不回收,就存在一条对V的强引用通路,会导致内存泄漏,因此要手动调用remove) | WeakReference |
虚引用 | 无法通过虚引用来调用对象,唯一的用处就是在GC时能收到系统的通知 | PhantomReference |
//通过-XX:PrintGCDetails
Heap
PSYoungGen total 59392K, used 14950K [0x0000000780700000, 0x0000000784700000, 0x00000007c0000000)
eden space 53248K, 28% used [0x0000000780700000,0x0000000781599b48,0x0000000783b00000)
from space 6144K, 0% used [0x0000000783b00000,0x0000000783b00000,0x0000000784100000)
to space 6144K, 0% used [0x0000000784100000,0x0000000784100000,0x0000000784700000)
ParOldGen total 131072K, used 2653K [0x0000000701400000, 0x0000000709400000, 0x0000000780700000)
object space 131072K, 2% used [0x0000000701400000,0x00000007016975c0,0x0000000709400000)
Metaspace used 3139K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 345K, capacity 388K, committed 512K, reserved 1048576K
由上图日志,我们可以知道堆内存空间分配:
具体的回收过程在后面的回收算法中讲到。
GC又分为 minor GC 和 Full GC (也称为 Major GC )
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
a.调用System.gc时,系统建议执行Full GC,但是不必然执行
b.老年代空间不足
c.方法去空间不足
d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
一共有:标记-清除算法,标记-整理算法,复制算法,分代收集算法。
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 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的操作。
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
串行收集器是最古老,最稳定以及效率高的收集器
可能会产生较长的停顿,只使用一个线程去回收
-XX:+UseSerialGC
-XX:+UseParNewGC(new代表新生代,所以适用于新生代)
Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,需要多核支持
-XX:ParallelGCThreads 限制线程数量
类似ParNew
新生代复制算法
老年代标记-压缩
更加关注吞吐量
-XX:+UseParallelGC
-XX:+UseParallelOldGC
2.3 其他GC参数
-XX:MaxGCPauseMills
-XX:GCTimeRatio
这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优
CMS运行过程比较复杂,着重实现了标记的过程,可分为
这里就能很明显的看出,为什么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)复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
UseSerialGC:Client模式下的默认值,使用Serial + Serial Old的收集器组合进行内存回收。
UseParNewGC:使用ParNew + Serial Old的收集器组合进行内存回收。
UseConcMarkSweepGC:使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现Concurrent Mode Failure失败后的后备收集器使用。
UseParallelGC:Server模式下的默认值,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收。
UseParallelOldGC:使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收。
SurvivorRatio:设置新生代中Eden与Survivor的比例,默认8。
PretenureSizeThreshold:设置直接进入老年代的对象大小。
MaxPretenureThreshold:设置晋升到老年代的对象年龄。
UseAdaptiveSizePolicy:是否动态调整Java堆中各个区域的大小以及进入老年代的年龄。
HandlePromotionFailure:是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况。
ParallelGCThreads:设置并行GC时进行内存回收的线程数。
GCTimeRatio:仅在使用Parallel Scavenge收集器时生效,GC时间占总时间的比率,默认99,即允许1%的GC时间。
MAXGCPauseMillis:仅在使用Parallel Scavenge收集器时生效,设置GC最大停顿时间。
CMSInitiatingOccupancyFraction:仅在使用CMS收集器时生效,设置老年代空间被使用多少后触发垃圾收集,默认68%。
UseCMSCompactAtFullCollection:仅在使用CMS收集器时生效,完成垃圾收集后是否要进行一次内存碎片整理。
CMSFullGCsBeforeCompaction:仅在使用CMS收集器时生效,设置收集器在进行若干次垃圾收集后再启动一次内存碎片整理。