一文了解垃圾回收算法、垃圾收集器

目录

​编辑原理

如何判定垃圾

引用计数法

缺陷

可达性分析

一个对象非死不可?对象的自我救赎

finalize的作用

finalized的问题

finalize的执行过程(生命周期)

垃圾收集法

标记清除

复制算法

标记整理

分代收集算法

垃圾收集器

Serial

ParNew

Parallel Scavenge

Serial Old

Parallel Old

CMS

G1

如何选择合适的垃圾收集器

常见垃圾回收器组合参数设定:(1.8)



原理

  • Java中的对象是通过引用关系来管理的。当一个对象不再被任何引用所指向时,它就被认为是不再被使用的,即成为了“垃圾”。
  • JVM通过垃圾回收器来自动检测并回收这些不再被使用的对象所占用的内存空间。
  • 当堆内存中的空闲空间不足时,JVM会触发垃圾回收过程,以释放被废弃对象所占用的内存。
  • 垃圾回收的触发条件可能因不同的垃圾回收器和JVM实现而有所不同。
     

如何判定垃圾

一个对象没有任何指针对其引用,它就是垃圾。
示例

不被需要和使用的对象就是垃圾,我们怎么去找到这些垃圾呢?

引用计数法


这个对象给它做一个计数引用,目前有三个引用指向,那么这里给他记录一个引用树为3。
接下来如果有一个引用消失了变成二,有一些引用消失变成一,最后引用消失变成零,当它变成零的时候,这对象成为了垃圾。
总结:
如果一个对象没有引用指向它的时候,或者说引用计数器里面的值为0的时候,表示该对象就是垃圾。

缺陷


当有循环引用的时候,导致无法回收掉本该是垃圾的对象。

那Java中是使用引用计数法来完成垃圾回收吗?来看代码:

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
    * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过
    */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new                             
        ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;objA = null;
        objB = null;
        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }
    public static void main(String[] args) {
        testGC();
    }
}

从上图可以看出,没有进行垃圾回收之前,内存占用11960K。进行垃圾回收之后,内存占用896K。说明对象确实被回收释放了。但如果按照引用计数算法,两个对象之间其实还存在着互相引用,即引用计数器的值为1,也就是说本来不应该被回收,所以这里使用的显然就不是引用计数算法。

可达性分析


能作为GC ROOT 类加载器、Thread 、虚拟机栈的本地变量表、static成员、常量引用、
Java是使用一种叫GC Root的算法,是什么意思呢?从根上的引用去找对象,能够被根节点引用找到的对象都不是垃圾,不用回收,如果是从根节点引用找不到的对象都是垃圾。

通过GC Root的对象,开始向下寻找,看某个对象是否可达
能作为GC Root:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

1.在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2.在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3.在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
4.在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
5.Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
6.所有被同步锁(synchronized关键字)持有的对象。
7.反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

一个对象非死不可?对象的自我救赎

        即使在可达性分析算法中不可达的对象,并不是”非死不可“,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为”没有必要执行“。
        如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

finalize的作用

  • finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
  • finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性
  • 不建议用finalize方法完成“非内存资源”的清理工作。

finalized的问题

  • 一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法
  • System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们
  • Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
  • finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行
  • 对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
  • finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)

由于Finalizer线程优先级相较于普通线程优先级要低,而根据Java的抢占式线程调度策略,优先级越低的线程,分配CPU的机会越少,因此当多线程创建重写finalize方法的对象时,Finalizer可能无法及时执行finalize方法,Finalizer线程回收对象的速度小于创建对象的速度时,会造成F-Queue越来越大,JVM内存无法及时释放,造成频繁的Young GC,然后是Full GC,乃至最终的OutOfMemoryError。

finalize的执行过程(生命周期)

大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
代码演示:

public class FinalizeEscapeGC {
	public static FinalizeEscapeGC SAVE_HOOK = null;


	public void isAlive() {
		System.out.println("yes, i am still alive :)");
	}
	
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("finalize method executed!");
		FinalizeEscapeGC.SAVE_HOOK = this;
	}

	public static void main(String[] args) throws Throwable {
		SAVE_HOOK = new FinalizeEscapeGC();//对象第一次成功拯救自己
		SAVE_HOOK = null;
		System.gc();// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("once, i am dead :(");
		}
		// 下面这段代码与上面的完全相同,但是这次自救却失败了
		SAVE_HOOK = null;
		System.gc();// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
		Thread.sleep(500);
		if (SAVE_HOOK != null) {
			SAVE_HOOK.isAlive();
		} else {
			System.out.println("second, i am dead :(");
		}
	}
}

        从结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。
        另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次逃脱成功,一次失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

垃圾收集法

标记清除

第一步、找到内存中需要被回收的对象,并且把它们标记出来。
        此时堆中的所有对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时。
第二步、清除掉被标记需要回收的对象,释放出对应的内存空间。

注意:
这里所谓的清除并不是真的置空,而是把需要清除对象的地址回收到空闲的地址列表里,下次有新对象需要加载时,直接使用这些地址即可,也就相当于一个覆盖的过程。
缺点:
(1)标记和清除两个过程都比较耗时,效率不高 。
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

将内存划分为两块相等的区域,每次只使用其中一块

当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
缺点:空间利用率降低,浪费空间。

标记整理

Mark-Compact
第一种算法会导致碎片化,第二种算法又浪费内存空间,那标记整理呢。第一步、标记(标记的过程仍然与“ 标记-清除”算法一样),但是后续步骤不是直接对可回收对象进行清理。
第二步、将所有的存活对象压缩到内存的一端,按顺序排放之后,清理边界外所有的空间。

优点:没有碎片化,也不浪费内存空间。
缺点:但是效率比较低,甚至比Copying更低。

分代收集算法

  • 思想:基于对象存活周期的长短,将内存分为新生代和老年代两个区域。新生代通常包含大量新创建的对象,老生代包含长时间存活的对象。垃圾回收器根据不同代的特点采用不同的回收策略。
  • 优点:提高了垃圾回收的效率,减少了不必要的内存清理。
  • 实现:新生代采用复制算法,老生代采用标记-压缩算法(或标记-清除算法)。

在内存划分出不同的区域,然后将回收的对象依据其年龄(年龄既是对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。如果一个区域中大多数对象都是朝生夕死的,难以熬过垃圾收集器过程,那么将它们集中在一起,每次回收只关注保留下存活的对象。而不是去标记那些大量的将要被回收的对象,就能以最小的代价会回收到大量的空间;如果剩下的都是难以消灭的对象,那么把他们集中在一起,虚拟机就可以使用较低的频率来回收这个区域,就兼顾了时间开销和内存空间的有效利用。


Java虚拟机设计者一般会把Java堆分为新生代(Young Generation)和老年代(Old Generation)

在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
部分收集(Partial [ˈpɑːʃl] GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
老年代收集(Major [ˈmeɪdʒə(r)] GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
整堆收集(Full GC):收集整个Java堆有可能会触发方法区的垃圾收集。


        动态对象年龄判定(虚拟机并不会永远地要求对象的年龄都必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄的所有对象的大小总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代),空间分配担保,老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minor GC,否则会进行full GC。

Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

垃圾收集器

垃圾收集器,随着内存大小的不断增大而演进。

串行收集器 Serial 和 Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。(适用于内存较小的嵌入式设备)


并行收集器[吞吐量优先] Paraller Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待阶段。(适用于科学计算、后台处理等若干交互场景)


并发收集器 [停顿时间优先] CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行。(适用于相对时间有要求的场景,比如WEB)

Serial

复制算法
新生代
单线程收集器
特点:它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在垃圾收集的时候需要暂停其他线程。
优点:简单高效
缺点:收集过程需要暂停所有线程
应用:Client模式下的默认新生代收集器(Serial收集器是最基本、发展历史最悠久的收集,增加(JDK1.3.1之前)是虚拟机新生代收集器的唯一选择。)

ParNew

复制算法
新生代
多线程收集器
特点:ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:
HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与erial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。优点:多CPU时,比Serial效率更高。
缺点:收集过程暂停所有的应用程序,单CPU核心时比Serial效率差。应用:运行在Server模式下的虚拟机中首选的新生代收集器。

Parallel Scavenge


收集器与吞吐量关系密切,故也称为吞吐量优先收集器。
复制算法
新生代
多线程收集器
关注吞吐量
特点:多线程

Parallel Scavenge收集器使用两个参数控制吞吐量:

XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间XX:GCRatio 直接设置吞吐量的大小。
吞吐量 = 运行用户代码时间 / 运行用户代码时间 + 运行垃圾收集时间。
如果虚拟机完成某个任务,用户代码加上垃圾收集器总共耗时100分钟,其中垃圾收集器花费了1分钟,那吞吐量就是 99 / 100= 99%。
吞吐量越大,意味着垃圾收集的时间更短、则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量。

最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis
直接设置吞吐量大小的 -XX:GCTimeRatio参数。

Serial Old

Serial Old 是Serial 收集器的老年代版本
标记整理
老年代
单线程收集器

Parallel Old


标记整理
老年代
多线程收集器
关注吞吐量

特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合

CMS


是一种以获取最短回收停顿时间为目标的收集器
Concurrent Mark Sweep
标记清除
老年代
并发收集器
关注最短停顿时间
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来良好的体验。比如web服务,b/s结构。


工作分为四步:
第一步、初始标记(STW),标记GC Roots能直接关联到的对象,速度非常快。
第二步、并发标记,进行GC Roots Tracing ,就是从GC Roots开始找到它能引用的所有对象的过程。
第三步、重新标记(STW),为了修成并发标记期间因用户程序继续运作导致标记产生变动的一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间要短。
第四步、并发清除,在整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程与用户线程一起并发执行的。

所谓的安全点呢就是我们的对象引用关系不会再发生变化
优点:并发收集、并发清除、低停顿。
缺点:对CPU要求高,无法处理浮动垃圾、产生大量空间碎片、并发阶段会降低吞吐量。

1.对CPU敏感,并发阶段虽然不会导致用户线程暂停,但是它总是要线程执行,还是会占用CPU资源,(一定程度上也是,吞吐量的下降)


2.无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次 gc 的时候清理掉。
        浮动垃圾需要设置一个百分比来确保程序的可执行,避免CMS运行时,预留的内存无法满足程序的需要(新对象分配空间),在JDK1.5中,这个百分比为68%,到 1.6 时,这个参数值提高到了 92%。那如果万一有些人说我就要把这个参数值设置 100%,那执行的时候内存满了,用户的线程运行不了。这时候会临时启用 serial Old (单线程执行,会暂停所有用户的线程)收集器来重新进行老年代的垃圾收集,这样停顿时间就更长了。


3.产生大量空间碎片、并发阶段会降低吞吐量。
CMS默认晋升老年代为6的原因:
简单来说,CMS对内存尤其敏感,且会导致单线程Serial FullGC 这个是非常严重的后果,而从结果上说越大的MaxTenuringThreshold会更快的导致heap的碎片化(不光old区,首先要明白对于内存的分配并不是真的一个对象一个对象紧密排列的),所以历代CMS默认这个值都会比较小(JDK8以前是4,之后调整为6)

G1


分代收集(仍然保留分代的概念)
并行与并发
老年代和新生代
关注最短停顿时间
内存分为Region[ˈriːdʒən] 区(内存是否连续)
可以设置最短停顿时间

工作分为四步
第一步、初始标记(STW),标记GC Roots能直接关联到的对象(速度很快)。
第二步、并发标记,进行 GC Roots Tracing,就是从GC Roots开始找到它能引用的所有其他对象的过程。
第三步、最终标记(STW),为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍微长,但是要比并发标记要短。
第四步、筛选回收(STW),对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间指定回收计划。

G1 总结:
JDK 7 开始使用,JDK8非常成熟,JDK9默认的垃圾收集器。
如果停顿时间过短,会造成频繁垃圾回收,会导致OOM:GC overhead limitexceeded (超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常)
判断是否需要使用G1收集器?
答:50%以上的堆被存活对占用、对象分配和晋升的速度变化非常大、垃圾回收时间较长。

如何选择合适的垃圾收集器

如果应用程序的内存在100M左右,使用串行收集器 -XX:+UseSerialGC。如果是单核心,并且没有停顿要求,默认收集器,或者选择带有选项的-XX:+UseSerialGC
如果允许停顿时间超过1秒或者更长时间,默认收集器,或者选择并行-XX:+UseParallelGC
如果响应时间最重要,并且不能超过1秒,使用并发收集器 -XX:+UseConcMarkSweepGC or -XX:+UseG1GC

可参考:Ergonomics
 

常见垃圾回收器组合参数设定:(1.8)


-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器

-XX:+UseParNewGC = ParNew + SerialOld
这个组合已经很少用(在某些版本中已经废弃)


-XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认 【PS +SerialOld】)
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
-XX:+UseG1GC = G1
Linux中没找到默认GC的查看方法,而windows中会打印UseParallelGC
    java +XX:+PrintCommandLineFlags -version
    通过GC的日志来分辨
Linux下1.8版本默认的垃圾回收器到底是什么?
    1.8.0_181 默认(看不出来)Copy MarkCompact
    1.8.0_222 默认 PS + PO

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值