深入Java虚拟机(四):垃圾回收

目录

 

判断对象是否存活

1、引用计数法

2、可达性分析法

四种引用类型

1、  强引用

2、  软引用(SoftReference)

3、  弱引用(WeakReference)

4、  虚引用(幽灵/幻影引用PhantomReference)

finalize()方法

回收方法区

垃圾收集算法

1、标记—清除算法

2、复制算法

3、标记—整理算法(Mark-Compact)

4、分代收集

垃圾收集器

1、Serial收集器(复制算法)

2、ParNew收集器 (复制算法)

3、Parallel Scavenge收集器(复制算法)

4、Serial Old收集器(标记-整理算法)

5、Parallel Old收集器 (标记-整理算法)

6、CMS收集器(标记-清除算法)

7、G1收集器 (标记-整理算法)

8、ZGC收集器


判断对象是否存活

1、引用计数法

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,即表示该对象可视为”垃圾“被回收。

引用计数器算法实现简单,效率高;但是不能解决循环引用问题(A 对象引用B 对象,B 对象又引用A 对象,但是A,B 对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1 之后,这个算法已经不再使用了。

2、可达性分析法

通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径称为引用链 ,当一个对象没有被GC Roots 的引用链连接(不可达)的时候,说明这个对象是不可用的。

可作为GC Roots的对象包括以下四种:

           (1)虚拟机栈中的引用的对象 
           (2)方法区中的类静态属性引用的对象。 
           (3)方法区中常量引用的对象。 
           (4)本地方法栈中JNI(即一般说的Native方法)的引用的对象。

深入Java虚拟机(四):垃圾回收_第1张图片

四种引用类型

1、  强引用

只要某个对象有强引用与之关联,JVM必定不会回收这个对象,JVM宁愿抛出OutOfMemory错误也不会回收这种对象

Object obj = new Object();

   如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

  比如ArrayList类的clear方法中就是通过将引用赋值为null来实现清理工作的:

private transient Object[] elementData;
public void clear() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
}

2、  软引用(SoftReference)

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。在Java中用java.lang.ref.SoftReference类来表示。

import java.lang.ref.SoftReference;
 
public class Main {
    public static void main(String[] args) {
         
        SoftReference sr = new SoftReference(new String("hello"));
        System.out.println(sr.get());//如果内存空间足够,就返回对象
    }
}


这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

3、  弱引用(WeakReference

当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

import java.lang.ref.WeakReference;
 
public class Main {
    public static void main(String[] args) {
     
        WeakReference sr = new WeakReference(new String("hello"));
         
        System.out.println(sr.get());//hello
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());//null
    }
}

弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。

  对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java中提供了WeakHashMap来满足这一常见需求。

弱引用还能用来在回调函数中防止内存泄露。

4、  虚引用(幽灵/幻影引用PhantomReference)

 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收.

要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

可以解决 finalize方法清理工作的实际运行时间也是不能预知的问题。详见https://blog.csdn.net/imzoer/article/details/8044900

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
 
 
public class Main {
    public static void main(String[] args) {
        ReferenceQueue queue = new ReferenceQueue();
        PhantomReference pr = new PhantomReference(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

 

finalize()方法

   真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并判断此对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“不需要要执行”。

如果这个对象被判定为需要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。因此调用finalize()方法不代表该方法中代码能够完全被执行。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。注意任何对象的finalize()方法只会被系统自动执行1次。

回收方法区

1、主要回收两部分内容:废弃常量和无用的类

2、判断类可以回收的三个条件:

(1)该类的所有实例都已经被回收

(2)加载该类的ClassLoader被回收

(3)该类的Class对象没有在任何地方被引用。

垃圾收集算法

1、标记—清除算法

包括两个阶段:

(1)标记需要回收的对象(GC Roots向下查找)

(2)清除标记对象

缺点:

(1)标记和清除两个过程的效率都不高

(2)产生大量空间碎片

2、复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。

3、标记—整理算法(Mark-Compact)

标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。

标记整理算法与标记清除算法最显著的区别是:标记清除算法不进行对象的移动,并且仅对不存活的对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存活对象进行处理,因此其不会产生内存碎片。

4、分代收集

深入Java虚拟机(四):垃圾回收_第2张图片

  新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照 8:1:1 的比例分为一个Eden区和两个Survivor(survivor0,survivor1)区,大部分对象在Eden区中生成。在进行垃圾回收时,将Eden区和Survivor区中还存活着的对象一次性复制到另一块Survivor空间上,然后清理掉Eden区和刚才用过的Survivor区。特别地,当另一块survivor区也不足以存放eden区和survivor区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。

对象何时进入老年代?

1、大对象直接进入老年代,防止对象在Eden区和两个Survivor区之间来回拷贝。

2、对象在Survivor区中每经历一次Minor GC,年龄就增加一岁。但年龄到达一定阈值(或者大于拥有相同年龄且大小大于Survivor空间的一半的对象的年龄)

Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。

垃圾收集器

深入Java虚拟机(四):垃圾回收_第3张图片

1、Serial收集器(复制算法)

新生代单线程收集器,优点是简单高效。收集几十兆甚至一百兆的新生代停顿时间在几十毫秒到一百多毫秒以内。

深入Java虚拟机(四):垃圾回收_第4张图片

2、ParNew收集器 (复制算法)

新生代并行收集器,ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

深入Java虚拟机(四):垃圾回收_第5张图片

3、Parallel Scavenge收集器(复制算法)

新生代并行收集器,追求高吞吐量,高效利用 CPU完成运算,适用于后台而不需要太多交互的任务。

深入Java虚拟机(四):垃圾回收_第6张图片

 

4、Serial Old收集器(标记-整理算法)

老年代单线程收集器,Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生ConcurrentMode Failure时使用。

5、Parallel Old收集器 (标记-整理算法)

老年代并行收集器,吞吐量优先

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。

6、CMS收集器(标记-清除算法)

老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

深入Java虚拟机(四):垃圾回收_第7张图片

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
运作过程分为4个步骤,包括: 
a)初始标记(CMS initial mark) :标记GC Roots能直接关联到的对象
b)并发标记(CMS concurrent mark) :进行GC Roots Tracing
c)重新标记(CMS remark) :修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录。
d)并发清除(CMS concurrent sweep)

CMS收集器存在3个缺点: 
1 对CPU资源敏感。一般并发执行的程序对CPU数量都是比较敏感的 
2 无法处理浮动垃圾。在并发清理阶段用户线程还在执行,这时产生的垃圾无法清理。 
3 由于标记-清除算法产生大量的空间碎片。

7、G1收集器 (标记-整理算法)

Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

深入Java虚拟机(四):垃圾回收_第8张图片

G1是一款面向服务端应用的垃圾收集器。 
G1收集器的运作大致可划分为以下几个步骤:

a)初始标记(Initial Marking) 
b)并发标记(Concurrent Marking) 
c)最终标记(Final Marking) 
d)筛选回收(Live Data Counting and Evacuation)

8、ZGC收集器

正如我们所知道的在JDK 11中即将迎来ZGC(The Z Garbage Collector),这是一个处于实验阶段的,可扩展的低延迟垃圾回收器。

* 每次GC STW的时间不超过10ms
* 能够处理从几百M到几T的JAVA堆
* 与G1相比,吞吐量下降不超过15%
* 为未来的GC功能和优化利用有色对象指针(colored oops)和加载屏障(load barriers)奠定基础
* 初始支持Linux/x64

 

参考资料

https://blog.csdn.net/u011534095/article/details/78845080

你可能感兴趣的:(JVM)