写点什么

JVM 垃圾回收

发布于: 2020 年 11 月 29 日
JVM垃圾回收

Java的最大特性是内存自动分配和垃圾自动回收,程序员不需要手动去处理这两件事情。

一.概览

垃圾回收主要包含了三部分:

  1. 如何标识出垃圾对象

  2. 何时进行垃圾回收

  3. 如何进行垃圾回收

二.如何标识出垃圾

如何在内存中识别出那些对象是要被回收的垃圾对象,那些是要被留下来有用对象呢?

JVM主要有两种算法来标识对象,1:引用计数算法。2:GcRoot可达性分析。

1.引用计数算法

描述:引用计数算法是指给对象中添加一个引用计数器,每当有个地方引用它的时候,该对象的计数器的值就+1,如果取消对该对象引用时,该引用计数器-1,一旦该对象的引用计数器为0的时候,那么此对象就可以被标识为可回收对象。

优点:引用计数算法实现简单,而且判断效率高。

缺点:很难解决对象间相互循环引用的问题。

示例:如对象objA和objB都有字段instance,赋值objA.instance = objeB,objB.instance = objA,除了上面的两个引用再无其他引用,实际上这两个对象已经不可能再被访问,但是两个对象互相引用,导致他们的引用计数器不为0,采用引用计数算法就无法通知GC收集器进行回收。下面的实例只能说明目前的垃圾回收器未使用引用计数算法。

-XX:+PrintGCDetails --打印GC详细日志



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 main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假设在这行发生GC,objA和objB是否能被回收
System.gc();
}
}
--运行结果
[GC (System.gc()) [PSYoungGen: 7916K->1080K(36864K)] 7916K->1088K(121856K), 0.0012717 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1080K->0K(36864K)] [ParOldGen: 8K->925K(84992K)] 1088K->925K(121856K), [Metaspace: 3239K->3239K(1056768K)], 0.0040313 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 36864K, used 952K [0x00000000d6e00000, 0x00000000d9700000, 0x0000000100000000)
eden space 31744K, 3% used [0x00000000d6e00000,0x00000000d6eee2d0,0x00000000d8d00000)
from space 5120K, 0% used [0x00000000d8d00000,0x00000000d8d00000,0x00000000d9200000)
to space 5120K, 0% used [0x00000000d9200000,0x00000000d9200000,0x00000000d9700000)
ParOldGen total 84992K, used 925K [0x0000000084a00000, 0x0000000089d00000, 0x00000000d6e00000)
object space 84992K, 1% used [0x0000000084a00000,0x0000000084ae7440,0x0000000089d00000)
Metaspace used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 347K, capacity 388K, committed 512K, reserved 1048576K

2.可达性分析

描述:可达性分析是通过一系列称为`GC Roots`的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路线称为引用链(Reference Chain),当一份对象到GC Root没有任何引用链存在,则证明此对象不可用。

可作为GC Roots的对象如下:

  1. 虚拟机栈(栈中的本地变量表中)中引用的对象。

  2. 方法区中类静态属性引用的对象。

  3. 方法区中常量引用的对象。

  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

优点:可以有效解决引用计数算法中对象间相互引用的问题。

缺点:难度稍大,复杂度较高。

如图:

GC ROOTS

三.何时进行垃圾回收

  1. cpu空闲时进行垃圾回收。

  2. 堆满的时候进行垃圾回收。

  3. 程序显式调用System.gc();是进行垃圾回收。



四.如何进行垃圾回收

此处主要是将垃圾回收算法,包含1:标记-清除,2:标记-整理,3:复制,4:分代

1.标记-清除

标记-清除算法包含两部分,一是标记,二是清除,首先是先标记出所需要回收的对象,然后回收所标记的对象。

缺点:1:效率问题,标记和清除效率都不高;2:空间问题,容易产生垃圾碎片,空间碎片太多容易导致分配大对象时,无法找到足够大的连续的内存空间,不得已触发另一次垃圾回收动作。(碎片指的不连续的一小块一小块内存)。

标记-清除-百度图片

2.标记-整理

标记整理算法同样分为两步,一是标记,二是整理,和标记-清除算法的区别是:整理不是直接对对象进行清理,而是让所有存活的对象都移向一端,然后直接清理掉端边界以外的内存对象。

优点:可以很好的避免垃圾碎片产生,也可以不想复制算法那样浪费掉一半的空间

标记-整理-百度示意图

3.复制算法

复制算法是指将内存分为大小相等两块,每次只是用一块,当一块内存用完后,将存活的对象移动到另一块,然后一次将其清理掉。

优点:简单、高效、不存在垃圾碎片。

缺点:每次只是用内存的一半,造成了内存浪费。



4.分代算法

分带算法其实是通过以上算法优化后的算法,是按照对象的生命周期分成响应的部分,一般分为新生代和老年代,新生代中的对象往往是朝生夕死的,只有少量对象可以存活,而老年代往往是相反的,存活率高。通常新生代由复制算法实现(新生代存活对象少,复制成本低),老年代由标记-清除或标记-整理算法时间。

现在较好的商业虚拟机中都采用了分代回收算法。

分代回收算法中的新生代又可以分为 Eden区,To survivor、From survivor,三者大小占比为8:1:1。

工作流程:

1:对象分配首先在Eden区中分配。

2:如果Eden区满了后会进行一次minorGC,将存活对象放入到To survivor区中,清理掉Eden区。

3:对象分配继续在Eden中分配,如果Eden区中又满了,那么将Eden区中存活对象和To survivor区中的存活对象复制到from survivor区中,清理掉Eden区和To survivor区。

4:继续执行1->2->3流程

5:说明:Java对象头中包含了GC回收年龄,如果此年龄大于15的话就将此对象移到老年代。



五.回收方法区

此部分是重读<<深入理解Java虚拟机>>这本书的时候拾漏的。

一般情况垃圾回收是发生在堆中的,堆中新生代一般每次可以回收70%到95%的对象。方法区中也是会进行垃圾回收的,但是方法区中垃圾回收性价比一般很低。

在大量使用反射、动态代理、CGLIB等再运行时生成大量动态类(或代理类)的情况下,必须具备卸载类的功能,以防止永久代溢出。

永久代(元数据区)的垃圾回收主要回收两部分内容:废弃常量和无用的类。

废弃常量:如果有一个字符串"abc"已经进入了常量池,但是当前系统没有任何一个String对象引用常量池中的"abc"常量,如果发生垃圾回收时,这个"abc"常量会被系统清理出常量池。

无用的类:判断一个类是无用的类比较苛刻,需满足如下3个条件:

  1. 该类所在的实例都被回收,也就是Java堆中不存在该类的任何实例。

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

  3. 该类对应的java.lang.Class对象没有在任何地方引用。



发布于: 2020 年 11 月 29 日阅读数: 23
用户头像

好记性不如烂笔头 2018.05.04 加入

还未添加个人简介

评论

发布
暂无评论
JVM垃圾回收