Java-GC-垃圾回收机制

Java垃圾回收机制

引言

“两者之间存在一堵由内存分配与GC技术筑建起来的高墙,墙里面的人想出去,墙外面的人却想进来”。

理解GC机制,必须要对JAVA内存区域很熟悉,如果不熟悉JAVA内存区域,建议先看上一篇文章《Java内存区域》


GC概念

  • GC的区域
  • GC的对象
  • GC的时机
  • GC算法
  • GC执行器

GC的区域

JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

GC分析算法

  • 引用计数算法
    • 该算法已经废弃,如果两个垃圾对象互相引用,将发生内存泄漏。
  • 可达性分析
    • 从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在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

由上图日志,我们可以知道堆内存空间分配:

  • Heap内存分为
    • 新生代
      • eden 是对象创建时的区域
      • from 新生代成长存活区
      • to 新生代成长存活区
    • 老年代
      • 达到一定纪元的对象由新生代进入老年代
    • 永久代(元数据)
      • 包含一些类空间,这一部分内存回收率最低

具体的回收过程在后面的回收算法中讲到。


方法区回收

  • 方法区回收主要是回收常量,比如定义了常量但是在字节码分析中并不引用,会进行常量回收。
  • 类对象回收
    • 如果类加载器被回收,且并不通过反射调用类对象,那么类对象也将被回收。

GC的时机

  • 系统自身决定
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可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 手动调用System.gc()请求JVM进行GC,这时JVM不一定会执行GC。

GC算法

一共有:标记-清除算法标记-整理算法复制算法分代收集算法。

标记清除

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点
最大的优点是,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,更重要的是,这个算法并不移动对象的位置。

缺点
它的缺点就是效率比较低(递归与全堆对象遍历)。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。

图例

在这里插入图片描述


标记整理

标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

优点
该算法不会像标记-清除算法那样产生大量的碎片空间。
缺点
如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
图例

img

左边是标记阶段,右边是整理之后的状态。可以看到,该算法不会产生大量碎片内存空间。

复制算法

该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

注意:
这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。

优点

实现简单;不产生内存碎片

缺点
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。

图例

img


分代回收算法

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

具体过程:新生代(Young)分为Eden区,From区与To区

img

当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。

img

这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,

img

再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。

img

经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。

img

老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。


垃圾回收器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

1.Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器
可能会产生较长的停顿,只使用一个线程去回收
-XX:+UseSerialGC

  • 新生代、老年代使用串行回收
  • 新生代复制算法
  • 老年代标记-压缩

img

2. 并行收集器

2.1 ParNew

-XX:+UseParNewGC(new代表新生代,所以适用于新生代)

  • 新生代并行
  • 老年代串行

Serial收集器新生代的并行版本
在新生代回收时使用复制算法
多线程,需要多核支持
-XX:ParallelGCThreads 限制线程数量

img

2.2 Parallel收集器

类似ParNew
新生代复制算法
老年代标记-压缩
更加关注吞吐量
-XX:+UseParallelGC

  • 使用Parallel收集器+ 老年代串行

-XX:+UseParallelOldGC

  • 使用Parallel收集器+ 老年代并行

img

2.3 其他GC参数

-XX:MaxGCPauseMills

  • 最大停顿时间,单位毫秒
  • GC尽力保证回收时间不超过设定值

-XX:GCTimeRatio

  • 0-100的取值范围
  • 垃圾收集时间占总时间的比
  • 默认99,即最大允许1%时间做GC

这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优

3. CMS收集器

  • Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)
  • 使用标记-清除算法
  • 并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)
  • 老年代收集器(新生代使用ParNew)
  • -XX:+UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记的过程,可分为

  1. 初始标记(会产生全局停顿)
  • 根可以直接关联到的对象
  • 速度快
  1. 并发标记(和用户线程一起)
  • 主要标记过程,标记全部对象
  1. 重新标记 (会产生全局停顿)
  • 由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
  1. 并发清除(和用户线程一起)
  • 基于标记结果,直接清理对象

img

这里就能很明显的看出,为什么CMS要使用标记清除而不是标记压缩,如果使用标记压缩,需要多对象的内存位置进行改变,这样程序就很难继续执行。但是标记清除会产生大量内存碎片,不利于内存分配。

CMS收集器特点:

尽可能降低停顿
会影响系统整体吞吐量和性能

  • 比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半

清理不彻底

  • 因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理

因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够)

  • -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
  • 如果不幸内存预留空间不够,就会引起concurrent mode failure

一旦 concurrent mode failure产生,将使用串行收集器作为后备。

CMS也提供了整理碎片的参数:

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理

  • 整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction

  • 设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads

  • 设定CMS的线程数量(一般情况约等于可用CPU数量)

CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间去整理碎片,从本质上并没有改善时间。

4. G1收集器

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)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

img

(4)Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

(5)Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

img

(6)复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

总结回收器

  • Serial是最基础的单线程回收,停顿期长,但是回收内存率最高,因为直接回收。
  • Par并行回收器,可以通过参数调整新生代并行或者老年代并行,与串行比效率相对高一些。
  • CMS与G1是较为高效的,他们都关注了停顿期,都分为4个阶段
    • 初始标记
    • 并发标记
      • 这期间用户线程仍然可以运行
    • 最终标记
    • 帅选回收

GC机制的使用

  • 手动GC的谨慎,一般情况下不要依赖手动GC,因为手动GC并不一定会执行GC。
  • 手动释放引用,保证内存的及时释放。
    • ref =null;
  • 注意GC的停顿时间,特别在一些敏感的业务系统中,要尽可能减低GC的停顿时间,否则可能产生如请求超时等问题。
  • 避免使用一次性的大对象,如超长字符串、大数组。尽可能使用其它方式代替。
    • 原因:超长字符串、大数组都是需要一片相对连续的存储空间,在JVM实现中,超过阈值的大对象直接存放到老年代区,而如果只使用1次,会使得资源得到浪费。
  • finalize方法一般不要使用
    • 它可以用来回收资源,也可以再这里重新建立引用,“复活” 回收队列中的不可达对象。
  • GC参数调整 -XX:
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收集器时生效,设置收集器在进行若干次垃圾收集后再启动一次内存碎片整理。
发布了57 篇原创文章 · 获赞 32 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/rekingman/article/details/104198361