JVM day02 如何判断对象可以回收垃圾回收算法分代垃圾回收垃圾回收器

Posted halulu.me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM day02 如何判断对象可以回收垃圾回收算法分代垃圾回收垃圾回收器相关的知识,希望对你有一定的参考价值。

目录

如何判断对象可以回收

1、引用计数法

引用计数,就是指一个对象被其他对象使用时,就让这个对象的计数+1,如果不再被引用的时候就-1,当计数为0的时候,这个对象就会被垃圾回收掉。

缺陷:循环引用

循环引用会导致彼此之间的计数不能为0,不会被垃圾回收

2、可达性分析算法

1、可达性分析算法首先确定一系列根对象(GC Root ,肯定不能被垃圾回收的对象就是根对象)。
2、垃圾回收之前,会对堆内存的对象的对象进行一遍扫描,看看每个对象是否被根对象直接或间接地使用,如果是,那么这个对象就不能被回收,反之就可以作为垃圾被回收。

java虚拟机中的垃圾回收器采用可达性分析算法来探索所有存活的对象。

哪些对象可以作为GC Root?
工具

1、jps
2、jmap -dump:format=b,live,file=1.bin 进程id
3、MAT工具

GC Root

1、系统核心类,(System Class )
2、操作系统时的java对象,(JNI Global)
3、正在活动的对象,(Thread)
4、正在加锁或者等待锁的对象(Busy Monitor)
(Everything that has called wait() or notify() or that is synchronized)

3、JVM四种引用

JVM四种引用:强引用、软引用、弱引用、虚引用、(终结器引用)

强引用:(StrongReference)

创建一个对象(new),把这个对象通过 = 赋值给一个变量,那么这个变量就强引用了这个新创建的对象。
Object obj = new Object()

强引用的特点:

沿着GC Root根对象的引用链能够找到的对象(强引用),这个对象不会被垃圾回收,即使报OutOfMemory错误也不会被垃圾回收。只有当这个强引用都断开的时候才会被垃圾回收。


软引用:(SoftReference)

通过软引用所引用的对象,那么这个对象就是软引用
SoftReference softRef = new SoftReference<>(T t); 这样就可以把一个对象变成软引用
然后通过softRef.get()就能获得这个对象

当内存不够并且没有被其他强引用引用的时候,软引用所引用的对象会被垃圾回收。


弱引用:(WeakReference)

通过弱引用所引用的对象,那么这个对象就是弱引用
WeakReference weakRef = new WeakReference<>(T t); 这样就可以把一个对象变成弱引用
然后通过weakRef.get()就能获得这个对象

当没有强引用引用的时候,只要发生垃圾回收,不管内存是否充足,弱引用所用的对象会被垃圾回收。


软引用和弱引用会配合引用队列一起工作释放内存。
软弱引用本身也是一个对象,当软弱引用所引用的对象被回收掉的时候,软弱引用就会进入引用队列。然后再引用队列中被回收掉。(软弱引用本身也强引用GC Root,所以需要引用队列来进行垃圾回收)


虚引用:(PhantomReferece)

Direct Memory 释放内存时的cleaner对象
虚引用必须配合引用队列使用
任何时候所引用的对象都可以被GC回收,但是必须配合引用队列来释放直接内存。

ReferenceQueue queue = new ReferenceQueue ();

PhantomReference pr = new PhantomReference (object, queue);

当ByteBuffer被垃圾回收的时候,虚引用对象会进入引用队列。然后由ReferenceHandler线程在引用队列中找到新入队的虚引用对象,之后根据直接内存的地址调用unsafe的freememory()方法释放内存。


终结器引用:(FinalReference)

终结器引用必须配合引用队列使用

Object父类都有一个finallize()方法,也就是终结方法。
当一个对象重写了终结方法并且没有强引用的时候就可以被垃圾回收。
在垃圾回收之前会使用finallize()方法。

当这个对象被垃圾回收的时候,终结器引用会加入引用队列(这个时候对象并没有被垃圾回收)。然后由FinallizeHandler线程(优先级很低,执行的几率很低)在引用队列中查看是否有终结器引用,如果有,那么就是根据终结器引用找到这个对象,并且调用这个对象的finallize()方法,调用完毕后,第二次垃圾回收的时候,这个对象才被真正回收掉。(不推荐使用,几率太低)

4、软引用的使用

1、对一些不重要的数据(比如读取一些不重要的图片),如果使用强引用的话,那么这些图片就不会被垃圾回收,会占用很大的内存。如果使用软引用的话,在内存不够的时候会被释放掉,缓解内存压力。(只有需要图片的时候,才会占内存,不需要使用的时候就不占内存)
2、一次垃圾回收后,如果内存仍然不足,就会触发软引用的垃圾回收。
3、SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);

    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) 
        //  list --> SoftReference --> byte[]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) 
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            list.add(ref);
            for (SoftReference<byte[]> w : list) 
                System.out.print(w.get()+" ");
            
            System.out.println();

        
        System.out.println("循环结束:" + list.size());
    

软引用对象的清理(引用队列)

ReferenceQueue<byte[]> queue = new ReferenceQueue<>()

    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) 
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列,先进先出
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) 
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) 
            list.remove(poll);
            poll = queue.poll();
        

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) 
            System.out.println(reference.get());
        

    

5、弱引用的使用

WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
弱引用的使用与软引用类似

    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) 
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) 
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) 
                System.out.print(w.get()+" ");
            
            System.out.println();

        
        System.out.println("循环结束:" + list.size());
    

垃圾回收算法

1、标记清除

标记清除算法分为2个阶段:标记、清除

标记

扫描堆中的对象,如果对象没有被GC Root直接或间接引用,就会被当成垃圾进行标记

清除

对垃圾所占用的空间进行释放。
释放并不是对每个字节进行清零操作,清除的时候只需要把对象所占用内存起始和结束地址记录下来,放在空闲列表中,下次如果创建对象的时候,就会将这些对象放在空闲列表的地址中。(替代,而不是清零)

优点:

速度快

缺陷:

容易产生内存碎片。
(标记清除并不会对空闲列表进行整理工作,会造成空间不连续,从而产生内存碎片)


2、标记整理

标记清除算法分为2个阶段:标记、整理

标记

扫描堆中的对象,如果对象没有被GC Root直接或间接引用,就会被当成垃圾进行标记

整理

对垃圾所占用的空间进行释放,并对这些内存碎片进行移动整理。

缺陷:

由于整理涉及到对象的移动,所以垃圾回收的效率相对更低(对象的地址发生改变)


3、复制

复制算法把内存区划分成大小相同的2块区域,FROM区和TO区。

1、找到需要进行垃圾回收的对象,并进行标记
2、把FROM区存活对象复制到TO区中(复制中会进行内存碎片的整理)
3、删除FROM区的数据,并交换FROM区和TO区的位置(原理的FROM变成TO)

缺陷:

没有内存碎片,但是占用双倍的内存空间


分代垃圾回收

JVM会根据不同情况使用不同的垃圾回收算法

老年代:长时间使用的对象(垃圾回收次数少)
新生代:用完就可以丢弃的对象(垃圾回收频繁)
伊甸园:eden

Minor GC

1、当创建一个对象时,首先会放在新生代的伊甸园中,
2、当伊甸园的内存空间不够的时候,会触发一次新生代的垃圾回收(Minor GC),Minor GC采用可达性算法,标记需要进行垃圾回收的对象,然后使用复制算法,将幸存对象复制到TO区,并对幸存对象的寿命+1,然后交换位置变为FROM区。(并不是交换内存地址,而是FROM区和TO区指向的地址发生变化)
3、第二次垃圾回收(Minor GC),会标记伊甸园和FROM区中会被垃圾回收的对象。然后重复第2步动作。
4、当寿命预期值的时候(最大寿命参照垃圾回收器,动态判断),这个对象就会晋升到老年代。

Minor GC会引发stop the world
当触发MinorGC的时候必须暂停其他用户线程,当垃圾回收完毕后,其他线程才继续运行。(垃圾回收涉及对象的复制)(暂停时间非常短)

Full GC

1、当老年代的内存空间不够的时候,会先触发Minor GC,如果空间仍然不足,才会触发一次老年代的垃圾回收(Full GC),Full GC使用标记整理或者标记清除算法清理老年代的空间。
Full GC 也会触发STW(stop the world)

OOM

如果Full GC后内存还是不够,就会报OutOfMemory错误。

垃圾回收器

相关VM参数

GC–大对象

1、大对象在老年代空间足够,而新生代空间远远不够的时候,会直接晋升老年代。
2、大对象在老年代,新生代的空间不够的时候,会直接报OutOfMemory错误。
3、当内存溢出OOM发生在某个线程中,并不会导致主线程的停止。

安全点

安全点Safe Point

程序执行时并非在所有地方都能停顿下来开始GC , 只有在特定的位置才能停顿下来开始GC , 这些位置称为“ 安全点(Safepoint)。

安全点的选择

安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。

比如:
循环的末尾
方法临返回前
调用方法之后
抛异常的位置

1、串行(SerialGC)

1、单线程的垃圾回收器
2、适用场景:堆内存较小的时候,适合个人电脑

-XX:+UseSerialGC=Serial + SerialOld (开启串行垃圾回收器)
新生代:复制算法
老年代:标记整理

2、吞吐量优先(ParallelGC)

1、多线程的垃圾回收器
2、适用场景:堆内存较大,多核CPU,适合服务器
3、单位时间上,STW的时间最短
4、吞吐量:垃圾回收时间在总程序运行时间的占比,占比越低,吞吐量越高

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC(开启吞吐量的垃圾回收器)
新生代:复制算法
老年代:标记整理
-XX:+UseAdaptiveSizePolicy (动态调整伊甸园和幸存区的比例)
-XX:GCTimeRatio=ratio (调整垃圾回收的时间和总时间的占比,吞吐量)(公式:1/(1+ratio))(默认值是99)(一般设置为19)
-XX:MaxGCPauseMillis=ms (默认值是200ms)(调整最大暂停毫秒数)
-XX:ParallelGCThreads=n (控制GC线程数)

3、响应时间优先(CMS)

1、多线程的垃圾回收器
2、适用场景:堆内存较大,多核CPU,适合服务器
3、单次的STW(stop-the-world)时间最短
4、在GC的同时,用户线程也能并发执行
5、垃圾清理的同时会产生新垃圾(浮动垃圾),需要预留一个空间保存浮动垃圾

6、使用标记清除算法(会产生内存碎片)
7、当内存碎片过多的时候,会造成并发失败。
8、并发失败会使垃圾回收器退化成串行回收器Serialold,使得垃圾回收时间大大增加。

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
(开启响应时间优先CMS的垃圾回收器)
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
(并行的GC线程数,跟CPU核数一样)
(并发的GC线程数,一般threads是n的1/4)
-XX:CMSInitiatingOccupancyFraction=percent
(执行CMS垃圾回收的内存占比,剩余空间会保存浮动空间)
-XX:+CMSScavengeBeforeRemark
(在重新标记之前对新生代GC,减轻重新标记的压力)

初始标记的时候,其他用户线程被暂停(STW),并发标记的时候,其他线程可以并发运行,重写标记的时候,其他用户线程被暂停(STW),最后做一次并发清理。

4、G1垃圾回收器

G1:Garbage First(JDK 9 默认)

适应场景:

1、同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
2、超大堆内存,会将堆划分为多个大小相等的区域 (每个区域都可以作为伊甸园、幸存区或者老年代,加快标记速度)
3、整体上是 标记+整理 算法,两个区域之间是 复制 算法

-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

G1垃圾回收阶段

3个阶段:Young Collection、Young Collection + Concurrent Mark 、Mixed Collection

1、Young Collection(STW)

1、G1将堆内存划分为各个大小相同的区域,每个区域可以作为伊甸园、幸存区或者老年代。
2、新生代垃圾回收,伊甸园内存不够,会复制进幸存区。
3、当幸存区的内存不够,幸存区有一部分会晋升到老年代,一部分拷贝到另一个幸存区中。

2、Young Collection + CM(不会STW)

1、在 Young GC 时会进行 GC Root 的初始标记
2、老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的JVM 参数决定
-XX:InitiatingHeapOccupancyParcent=parcent(默认45%)

3、Mixed Collection

1、会对 E(伊甸园)、S(幸存区)、O(老年代)进行全面垃圾回收
2、最终标记(Remark)会 STW
3、拷贝存活(Evacuation)会STW
4、-XX:MaxGCPauseMillis=ms
5、老年代区会采用复制算法复制到新的老年代区(G1会根据最大暂停时间判断是否复制到新的老年代区)
6、G1会挑选垃圾最多的老年代区进行垃圾回收(释放空间最多)

CMS和G1在并发失败的时候才会产生full gc.

JDK 8u20 字符串去重

-XX:+UseStringDeduplication(默认打开)

将所有新分配的字符串放入一个队列
当新生代回收时,G1并发检查是否有字符串重复
如果它们值一样,让它们引用同一个 char[]
注意,与 String.intern() 不一样
String.intern() 关注的是字符串对象
而字符串去重关注的是 char[] 在
JVM内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark 默认启用

JDK 8u60 回收巨型对象

一个对象大于 region 的一半时,称之为巨型对象
G1 不会对巨型对象进行拷贝 回收时被优先考虑
G1 会跟踪老年代所有incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉

GC调优

1、Full GC 和Minor GC 频繁

这是因为新生代的内存空间比较紧张,由此导致Minor GC较频繁。Minor GC频繁也会造成晋升老年代机制降低,使得老年代存储了大量寿命比较短的对象,由此也会造成老年代的Full GC频繁。
解决方法: 增加新生代的内存空间。

2、请求高峰期发生Full GC,单次暂停时间特别长(CMS)

CMS有四个阶段:初始标记、并发标记、重新标记和并发清理。其中初始标记和并发标记都是比较快的,比较慢的是重新标记。当暂停时间比较长的时候,有可能发生在重新标记上。这是因为重新标记需要扫描整个堆内存,当业务高峰的时候,新生代对象比较多,重新标记耗时就比较长。
解决方法: 使用-XX:+CMSScavengeBeforeRemark指令在重新标记之前对新生代进行垃圾回收,这样新生代的对象就没那么多了。

3、老年代充裕情况下,发生Full GC(CMS JDK1.7)

jdk1.7永久代的空间不足也会导致Full GC。
jdk1.8之后使用的是元空间,空间大小相对充足。

以上是关于JVM day02 如何判断对象可以回收垃圾回收算法分代垃圾回收垃圾回收器的主要内容,如果未能解决你的问题,请参考以下文章

Day339.垃圾回收相关算法 -JVM

JVM如何判断哪些对象可以被回收

03 JVM 从入门到实战 | 简述垃圾回收算法

jvm如何判断对象是否可以被回收

JVM04——垃圾回收器和回收算法

漫画:什么是 JVM 的垃圾回收?