JVM——垃圾回收相关基础概念(gc、四种对象引用)总结

System.gc()

  • 默认情况下,通过调用System.gc()和Runtime.getRuntime().gc(),会显式的触发Full GC,同时对新生代和老年代进行垃圾回收,尝试释放死亡的对象占用的内存,以及方法区的回收(方法区垃圾回收条件比较严格而已)。
  • System.gc()并不能保证对垃圾收集器的调用。即这个垃圾回收行为不一定能马上执行,仅仅是告诉JVM希望进行一次垃圾回收操作,具体实施与否不一定。一般情况下不回去手动调用该方法进行垃圾回收,垃圾回收是JVM自动进行的。
public class Test {

    public static void main(String[] args) {
        new Test();
        System.gc();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("重写了finalize");
    }
}

System.gc()只是提醒JVM要垃圾回收,然后并不是一定执行的,这段代码可能要经过很多次的执行后才能看到finalize的执行。

如果在System.gc()之后加上System.runFinalization(),那么将强制调用失去引用对象的finalize()方法

public class Test {

    public static void main(String[] args) {
        new Test();
        System.gc();

        System.runFinalization();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("重写了finalize");
    }
}

手动gc时不可达对象的回收行为

public class Test {

    public void localVarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];//10m
        System.gc();
    }

    //JDK1.8
    public static void main(String[] args) {
        Test test = new Test();
        test.localVarGC1();
    }
}

控制台-XX:+PrintGCDetails打印GC信息:

[GC (System.gc()) [PSYoungGen: 19369K->10720K(75776K)] 19369K->11897K(249344K), 0.0077174 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 10720K->0K(75776K)] [ParOldGen: 1177K->11761K(173568K)] 11897K->11761K(249344K), [Metaspace: 4975K->4975K(1056768K)], 0.0134804 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 650K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 1% used [0x000000076b700000,0x000000076b7a2a68,0x000000076f680000)
  from space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
  to   space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
 ParOldGen       total 173568K, used 11761K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 6% used [0x00000006c2400000,0x00000006c2f7c640,0x00000006ccd80000)
 Metaspace       used 4983K, capacity 5108K, committed 5248K, reserved 1056768K
  class space    used 560K, capacity 596K, committed 640K, reserved 1048576K
  • 第一行YoungGen垃圾回收中,回收前19369k,回收后10720k,基本等于byte[]申请的10m大小,也就是说该对象还是存在于新生代中,并没有被回收掉
  • 的人行Full GC之后,新生代中没有内存占用,老年代中1177k变为了11761k,也就是将byte[]对象放入了老年代,仍然没有被回收

public class Test {

    public void localVarGC2() {
        byte[] buffer = new byte[10 * 1024 * 1024];//10m
        buffer = null;
        System.gc();
    }

    //JDK1.8
    public static void main(String[] args) {
        Test test = new Test();
        test.localVarGC2();
    }
}

控制台-XX:+PrintGCDetails打印GC信息:
[GC (System.gc()) [PSYoungGen: 19369K->1723K(75776K)] 19369K->1731K(249344K), 0.0023246 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1723K->0K(75776K)] [ParOldGen: 8K->1521K(173568K)] 1731K->1521K(249344K), [Metaspace: 4974K->4974K(1056768K)], 0.0084135 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 650K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 1% used [0x000000076b700000,0x000000076b7a2a68,0x000000076f680000)
  from space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
  to   space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
 ParOldGen       total 173568K, used 1521K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 0% used [0x00000006c2400000,0x00000006c257c6e0,0x00000006ccd80000)
 Metaspace       used 4982K, capacity 5108K, committed 5248K, reserved 1056768K
  class space    used 560K, capacity 596K, committed 640K, reserved 1048576K
  • 在Young GC的时候直接就由19369k将为了1723k,由于buffer对象失去了引用,则直接被Young GC回收掉了。

public class Test {

    public void localVarGC3() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];//10m
        }
        System.gc();
    }

    //JDK1.8
    public static void main(String[] args) {
        Test test = new Test();
        test.localVarGC3();
    }
}

控制台-XX:+PrintGCDetails打印GC信息:
[GC (System.gc()) [PSYoungGen: 19369K->10736K(75776K)] 19369K->11862K(249344K), 0.0095844 secs] [Times: user=0.09 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 10736K->0K(75776K)] [ParOldGen: 1126K->11761K(173568K)] 11862K->11761K(249344K), [Metaspace: 4973K->4973K(1056768K)], 0.0110976 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 650K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 1% used [0x000000076b700000,0x000000076b7a2a78,0x000000076f680000)
  from space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
  to   space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
 ParOldGen       total 173568K, used 11761K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 6% used [0x00000006c2400000,0x00000006c2f7c700,0x00000006ccd80000)
 Metaspace       used 4980K, capacity 5108K, committed 5248K, reserved 1056768K
  class space    used 560K, capacity 596K, committed 640K, reserved 1048576K
  • 代码块中申请分配的10m内存,并没有在Young GC后被回收,而是在Full GC后进入了老年代

代码块中的代码出了代码块不就失效了嘛?从字节码信息中观察如下:

虽然局部变量表中只有非静态方法默认在0位置的this变量,但是在方法信息中局部变量表的大小确为2,相差的哪一个就是buffer对象。buffer虽然是在代码块中定义的,但是由于在回收的时候他还未失效,仍旧占用着局部变量表1的位置,所以没有被垃圾回收


public class Test {

    public void localVarGC4() {
        {
            byte[] buffer = new byte[10 * 1024 * 1024];//10m
        }
        int i = 0;
        System.gc();
    }

    //JDK1.8
    public static void main(String[] args) {
        Test test = new Test();
        test.localVarGC4();
    }
}

控制台-XX:+PrintGCDetails打印GC信息:
[GC (System.gc()) [PSYoungGen: 19369K->1675K(75776K)] 19369K->1683K(249344K), 0.0014971 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 1675K->0K(75776K)] [ParOldGen: 8K->1521K(173568K)] 1683K->1521K(249344K), [Metaspace: 4976K->4976K(1056768K)], 0.0078832 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 650K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 1% used [0x000000076b700000,0x000000076b7a2a68,0x000000076f680000)
  from space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
  to   space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
 ParOldGen       total 173568K, used 1521K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 0% used [0x00000006c2400000,0x00000006c257c7f8,0x00000006ccd80000)
 Metaspace       used 4984K, capacity 5108K, committed 5248K, reserved 1056768K
  class space    used 560K, capacity 596K, committed 640K, reserved 1048576K
  • 可以看到当加了一行局部变量声明的代码之后,Young GC成功回收了buffer对象。因为局部变量i的定义,占用了局部变量表中1的位置(即局部变量表的深度还是2,一开始的时候表中1的位置是buffer对象,在i声明之后就覆盖了buffer),所以代码块中buffer对象也就是一个死亡对象了。

public class Test {

    public void localVarGC1() {
        byte[] buffer = new byte[10 * 1024 * 1024];//10m
        System.gc();
    }

    public void localVarGC5() {
        this.localVarGC1();
        System.gc();
    }

    //JDK1.8
    public static void main(String[] args) {
        Test test = new Test();
        test.localVarGC5();
    }
}

控制台-XX:+PrintGCDetails打印GC信息:
[GC (System.gc()) [PSYoungGen: 19369K->10744K(75776K)] 19369K->11855K(249344K), 0.0069230 secs] [Times: user=0.08 sys=0.02, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 10744K->0K(75776K)] [ParOldGen: 1111K->11762K(173568K)] 11855K->11762K(249344K), [Metaspace: 4975K->4975K(1056768K)], 0.0102326 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (System.gc()) [PSYoungGen: 0K->0K(75776K)] 11762K->11762K(249344K), 0.0002860 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 0K->0K(75776K)] [ParOldGen: 11762K->1522K(173568K)] 11762K->1522K(249344K), [Metaspace: 4975K->4975K(1056768K)], 0.0054901 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 650K [0x000000076b700000, 0x0000000770b80000, 0x00000007c0000000)
  eden space 65024K, 1% used [0x000000076b700000,0x000000076b7a2a78,0x000000076f680000)
  from space 10752K, 0% used [0x0000000770100000,0x0000000770100000,0x0000000770b80000)
  to   space 10752K, 0% used [0x000000076f680000,0x000000076f680000,0x0000000770100000)
 ParOldGen       total 173568K, used 1522K [0x00000006c2400000, 0x00000006ccd80000, 0x000000076b700000)
  object space 173568K, 0% used [0x00000006c2400000,0x00000006c257c8a0,0x00000006ccd80000)
 Metaspace       used 4983K, capacity 5108K, committed 5248K, reserved 1056768K
  class space    used 560K, capacity 596K, committed 640K, reserved 1048576K
  • localVarGC1中gc之后,buffer对象被移动到了老年代,再次gc之后,老年代11762k变为1522k,buffer对象被垃圾完全回收。

Stop the world

简称STW,指的是GC过程中会,会产生应用程序的停顿,应用程序的所有线程都被暂停(主要指的是所有用户线程),感觉像是全世界都静止了一样。

可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿:

  • 分析工作必须在一个能确保一致性的快照中进行,可达性分析算法分析对象就是限于某一个时刻,整个分析期间,系统看起来像被冻结在某个时刻。因为JVM中的数据是在时刻变化的,对象间的引用关系不断地变化,分析将毫无意义。
  • STW与垃圾回收器无关,所有的GC都会或多或少、或长或短的存在STW的情况。
    • G1、CMS等垃圾回收器也是不能完全避免STW,只能说垃圾回收器的回收效率越来越高,尽可能的缩短了STW的时间
  • 开发中没有很必要的情况不要使用System.gc(),会导致STW的发生,属于自己给自己找麻烦。

垃圾回收的并发与并行

在垃圾收集器的相关领域中并发与并行解释如下:

  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。也就是说在STW期间,有多条线程在处理垃圾收集任务(并行的垃圾回收器有ParNew、Parallel Scavenge、Parallel Old)
  • 串行:只有一条垃圾回收线程在处理垃圾回收任务。
  • 并发:指用户线程与垃圾回收线程同时执行(但不一定是并行,可能会与用户线程交替执行的),垃圾回收线程在执行时不会停止用户线程。
    • 如用户线程在继续执行,垃圾收集器线程运行在另一个cpu上
    • 代表的垃圾收集器有G1、CMS

安全点和安全区域

安全点

程序执行的时候并非是可以在所有地方都停下来进行GC,只有在特点的位置才能停顿下来进行GC,这些位置称为安全点。

安全点的选择很重要,如果太少可以能导致GC的等待时间太长,如果太密集会导致运行的性能下降。大部分指令的执行时间都非常的短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如选择一些执行时间较长的指令作为安全点(方法调用、循环跳转、异常跳转)

如何在GC发生的时候,检查所有线程都运行到了最近的安全点停顿下来了呢?

  • 抢先式中断(目前已经没有什么JVM在用了):首先中断所有线程,如果还有线程不在安全点,就恢复线程,让这些线程执行到安全点。
  • 主动式中断:设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果中断标识为真,则将自己进行中断挂起。

安全区域

安全点保证了程序执行时,在不太长的时间内就会遇到可进入GC的位置,但是程序“不执行”的时候呢,例如线程处于Sleep状态或者Blocked状态,这时候线程无法相应JVM的中断请求,JVM也不会等待线程被唤醒,对于这种情况来说就需要安全区域来解决问题。

安全区域是指一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。

  • 当线程运行到安全区域的代码时,首先标识已经进入了安全区域,如果这段时间内发生GC,JVM会忽略标识为安全区域状态的线程
  • 当线程即将离开安全区域时,会检查JVM是否已经完成了GC,如果完成了,则继续执行,否则线程必须等待直到收到可以安全离开安全区域的信号为止。

引用

Java中引用分为四种:强引用(Strong reference)、软引用(soft reference)、弱引用(weak reference)、虚引用(phantom reference),引用强度依次递减,我们日常编码中使用的一般都是强引用。

强引用

最常见的引用,也就是开发中最常见的声明赋值:Object obj = new Object()这样的引用关系,也是默认的引用方式,我们通常也是通过强引用直接访问目标对象的。

  • 在任何情况下,只要强引用的关系存在,垃圾回收器就不会回收掉被强引用的对象,即强引用的对象都是可触及的、可达的。死也不回收!即使是OOM。
  • 对于一个强引用的对象来说,如果没有其他的引用关系,只要超过了引用的作用域或者显式的将强引用赋值为null(obj = null;),就会被当作垃圾回收。当然具体回收的时机还是要看具体的垃圾回收策略。
  • 由于强引用的特点,通常Java内存泄漏就是由于一个生命周期很长的对象强引用到了其他对象造成的。

软引用

在系统将要发生内存溢出之前,将会把这些对象列入回收范围之内进行第二次回收,如果这次垃圾回收后还是内存资源不足,才会抛出OOM。所以软引用是不会导致OOM的,只有当内存资源不够的时候才会去回收软引用的可达对象

软引用是来描述一些“还有用”但是又不是“必须”的对象,软引用的对象也是可触及、可达的对象

通常是来实现内存敏感的缓存的,如高速缓存就有用到软引用,像Mybatis的源码中就有使用到软引用。

Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);//此时同时存在obj的强引用和softReference的软引用
obj = null;//释放掉强引用,仅存软引用
softReference.get();//获取obj软引用对象

//也可直接声明
SoftReference<Object> softReference2 = new SoftReference<>(new Object());

弱引用

只被弱引用关联的对象只能生存至下一次垃圾回收之前,当垃圾收集器工作的时候,无论内存资源是否足够,都会回收掉弱引用关联的对象,弱引用可以理解为垃圾收集器发现即回收

弱引用也是可以像软引用一直用作数据缓存。

Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);//此时同时存在obj的强引用和weakReference的软引用
obj = null;//释放掉强引用,仅存软引用
weakReference.get();//获取obj软引用对象

虚引用

一个对象是否有虚引用,完全不会对其生命周期造成影响,几乎等于没有引用,也无法通过虚引用获得一个实例对象为一个对象设置虚引用关联关系的唯一目的就是能在这个对象被回收的时候收到一个系统通知,用于追踪垃圾回收的过程

  • 当垃圾收集器准备回收一个对象的时候,发现该对象存在虚引用,就会在回收之后,将这个虚引用放入引用队列中,以便通知应用程序对象回收的情况。
  • 对于虚引用来说是拿不到对象的,get方法返回null
Object obj = new Object();
//必须同时声明一个引用队列
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
obj = null;
phantomReference.get();//这里是返回null的

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值