JVM_垃圾回收

1.要回收的内存区域

Java虚拟机的内存模型分为五个部分,分别是:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁,因此这三个区域不需要垃圾回收。
堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时。因此它们没办法根据线程的创建而创建、线程的结束而释放。堆中存放JVM运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定。 因此,堆和方法区的内存回收具有不确定性,因此垃圾回收器要负责这两个区域的垃圾回收。

2.堆内存回收

2.1堆内存回收判定方式

在对堆进行对象回收之前,首先要判断哪些是无效对象。一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收。一般有两种判别方式:

  • 引用计数法

      每个对象都有一个计数器,当这个对象被一个变量或另一个对象引用一次,该计数器加一;若该引用失效则计数器减一。当计数器为0时,就认为该对象是无效对象。

  • 可达性分析法

      所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象。 GC Roots是指:
      1.Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象)
      2.本地方法栈所引用的对象
      3.方法区中静态属性引用的对象
      4.方法区中常量所引用的对象

PS:注意!GC Roots并不包括堆中对象所引用的对象!这样就不会出现循环引用。
两者对比: ,
引用计数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题。
因此,目前主流语言均使用可达性分析方法来判断对象是否有效。

2.2堆内存回收过程

  1. 判断该对象是否覆盖了finalize()方法

    1. 若已覆盖该方法,并该对象的finalize()方法还没有被执行过,那么就会将finalize()扔到F-Queue队列中;
    2. 若未覆盖该方法,则直接释放对象内存。
  2. 执行F-Queue队列中的finalize()方法:虚拟机会以较低的优先级执行这些finalize()方法们,也不会确保所有的finalize()方法都会执行结束。如果finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除。
  3. 对象重生或死亡:如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。

注意:
强烈不建议使用finalize()函数进行任何操作!如果需要释放资源,请使用try-finally
因为finalize()不确定性大,开销大,无法保证顺利执行。

3.方法区内存回收

由于方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。
方法区中主要清除两种垃圾:

  1. 废弃常量
  2. 废弃的

3.1如何判断废弃常量

清除废弃的常量和清除对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。

3.2如何判断废弃的类

清除废弃类的条件较为苛刻:

  1. 该类的所有对象都已被清除
  2. 该类的java.lang.Class对象没有被任何对象或变量引用。只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除。
  3. 加载该类的ClassLoader已经被回收

4.垃圾回收算法

4.1标记-清除算法

“标记-清除”算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
JVM_垃圾回收
“标记-清除”算法的不足主要有两个:
效率问题:标记和清除这两个过程的效率都不高
空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次

4.2复制算法(新生代回收算法)

“复制”算法是为了解决“标记-清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
现在的商用虚拟机(包括HotSpot)都是采用这种收集算法来回收新生代
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下

  1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次出发Minor gc的时候,会扫描Eden区和From区,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden区和From区清空。
  2. 当后续Eden区又发生Minor gc的时候,会对Eden区和To区进行垃圾回收,存活的对象复制到From区,并将Eden区和To区清空
  3. 部分对象会在From区域和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还存活,就存入老年代。

4.3标记整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为“标记-整理算法”。标记过程仍与“标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

4.4分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法。

5.垃圾收集器

垃圾收集器分为新生代垃圾收集器,老年代垃圾收集器,通用垃圾收集器。重点掌握CMS垃圾收集器和G1垃圾收集器。

1. CMS垃圾收集器

CMS收集器是一款追求停顿时间的老年代收集器,它在垃圾收集时使得用户线程和GC线程并行执行,因此在垃圾收集过程中用户也不会感受到明显的卡顿。但用户线程和GC线程之间不停地切换会有额外的开销,因此垃圾回收总时间就会被延长。
垃圾回收过程

  • 1.初始标记

停止一切用户线程,仅使用一条初始标记线程对所有与GC ROOTS直接关联的对象进行标记。速度很快。

  • 2.并发标记

使用多条并发标记线程并行执行,并与用户线程并发执行。此过程进行可达性分析,标记出所有废弃的对象。速度很慢。

  • 3.重新标记

停止一切用户线程,并使用多条重新标记线程并行执行,将刚才并发标记过程中新出现的废弃对象标记出来。这个过程的运行时间介于初始标记和并发标记之间。

  • 4.并发清除

只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象。这个过程非常耗时。
CMS缺点

  • 1.吞吐量低

由于CMS在垃圾收集过程使用用户线程和GC线程并行执行,从而线程切换会有额外开销,因此CPU吞吐量就不如在垃圾收集过程中停止一切用户线程的方式来的高。

  • 2.无法处理浮动垃圾,导致频繁Full GC

由于垃圾清除过程中,用户线程和GC线程并发执行,也就是用户线程仍在执行,那么在执行过程中会产生垃圾,这些垃圾称为“浮动垃圾”。
如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足时,就需要再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC。

  • 3.使用“标记-清除”算法产生碎片空间

由于CMS使用了“标记-清除”算法, 因此清除之后会产生大量的碎片空间,不利于空间利用率。不过CMS提供了应对策略:
开启-XX:+UseCMSCompactAtFullCollection
开启该参数后,每次FullGC完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块儿。但每次都整理效率不高,因此提供了以下参数。
设置参数-XX:CMSFullGCsBeforeCompaction
本参数告诉CMS,经过了N次Full GC过后再进行一次内存整理。

2.G1垃圾收集器

G1的特点
1.追求停顿时间
2.多线程GC
3.面向服务端应用
4.标记-整理和复制算法合并。与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
5.可对整个堆进行垃圾回收
6.可预测停顿时间:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
G1的内存模型
G1垃圾收集器没有新生代和老年代的概念了,而是将堆划分为一块块独立的Region。当要进行垃圾收集时,首先估计每个Region中的垃圾数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率。
Remembered Set
一个对象和它内部所引用的对象可能不在同一个Region中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?
当然不是,每个Region都有一个Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在GC ROOTs中再加上Remembered Set即可防止对所有堆内存的遍历。
G1垃圾收集过程
1.初始标记:仅标记与GC ROOTS直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快。
2.并发标记:进行全面的可达性分析,找出存活的对象,开启一条并发标记线程与用户线程并行执行。这个过程比较长。
3.最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这一阶段需要停顿线程,但是可并行执行。
4.筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

6.Java中的引用类型

Java中根据生命周期的长短,将引用分为4类。

1.强引用

通过关键字new创建的对象所关联的引用就是强引用。 只要强引用存在,该对象永远也不会被回收。

2.软引用

只有当堆即将发生OOM异常时,JVM才会回收软引用所指向的对象。
软引用通过SoftReference类实现。
软引用的生命周期比强引用短一些。

3.弱引用

只要垃圾收集器运行,软引用所指向的对象就会被回收。
弱引用通过WeakReference类实现。
弱引用的生命周期比软引用短。

4.虚引用

虚引用也叫幽灵引用,它和没有引用没有区别,无法通过虚引用访问对象的任何属性或函数。
一个对象关联虚引用唯一的作用就是在该对象被垃圾收集器回收之前会受到一条系统通知。
虚引用通过PhantomReference类来实现。

相关推荐