JVM垃圾回收

一.判断对象存活的方法

引用计数法

给对象添加一个引用计数器,当对象被引用的时候计数器加1,引用失效时减1。计数器为0时对象可被回收(Python在使用)。

Python为了解决循环引用,专门开启一个线程去处理。

优点:快,方便,实现简单
缺点:对象相互引用时,很难判断对象是否该回收;开启一个线程去回收相关引用的对象,由于多开启了一个线程,效率并不高。

根可达性分析

什么是GC roots?

在Java语言中,"GC roots", 或者说tracing GC的"根集合", 是一组必须活跃的引用。可作为 GC Roots 的对象包括下面几种:

  • 在虚拟机栈(栈帧中的局部变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 本地方法栈中 JNI(Native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,“临时性”地加入的其他对象。

我们一般知道静态变量,线程中栈里面的变量,常量池里的引用,JNI引用的对象是GC roots就行了。

Class对象回收的条件

Java 虚拟机理论上会回收Class,Class要被回收,条件比较"苛刻",必须同时满足以下的条件:

1、该类的所有实例都已经被回收,即 Java 堆中不存在该类及其任何派生子类的实例

2、加载该类的类加载器已经被回收

3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

Java 虚拟机允许对满足上述三个条件的无用类进行回收,但并不是说必然被回收,仅仅是允许而已。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数禁用类的垃圾收集。

Finalize

根可达算法判断出不可达的对象,不一定立刻就会被回收,可以在类的Finalize中对它进行拯救。这个方法优先级很低,一般线程休眠得到时候才会执行。Finalize在垃圾回收器里面相当于是另外起来一个线程去处理,优先级比较低。

二.Java中各种引用

强引用:在java代码中直接就是=,与GC root强引用的对象不会被GC回收

软引用:SoftReference,在内存不足的时候(OOM),即使与GC root可达,也会被GC回收

弱引用:WeakReference,只要发生GC就会被回收

虚引用:PhantomReference,虚引用是最弱的一种java对象引用方式,虚引用的句柄是获取不到对象的,正如它的名字一样:形同虚设。它随时都可能会被回收。

使用:

// 新建一个对象
User obj = new User();
// 存储被回收的对象
ReferenceQueue QUEUE = new ReferenceQueue<>();
// phantomReference使用虚引用指向这个内存空间
PhantomReference phantomReference = new PhantomReference<>(obj, QUEUE);

我们拿虚引用的时候是拿不到的:

System.out.println(phantomReference.get()); // 获取不到 打印为null

虚引用主要用来跟综对象被垃圾回收的活动,判定垃圾回收器是否正常工作。虚引用与软引用和用的一个区别在于:引用必须和队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚用,来了解被引用的对象是否将要被垃圾回收,程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

三.对象的分配策略

JVM垃圾回收_第1张图片

热点数据和逃逸分析

所有的对象都会分配在堆里面吗?并不是全部的对象都会分配在堆上面的,极少一部分对象是分配在栈里面。

分配在栈里面的对象需要满足它所在的方法是JIT热点编译(循环次数非常大),接着满足逃逸分析。

如果这个方法一直在循环调用(10000次),就会触发JIT热点编译。接着对在这个方法里面的对象进行逃逸方法,也就是分析有没有其他方法直接或者间接的调用这个对象。如果没有调用,满足逃逸分析,直接分配在栈里面。这其实很好理解,堆是线程共享的,如果我这个对象只在这个方法里面调用,我直接分配在本线程的栈里面就好了,只给我这一个线程调用。这样做的好处是,我分配在栈里面,当方法调用完毕后,对象会随着栈帧出虚拟机栈而被回收,不需要进行GC。

对象优先在 Eden/TLAB 分配

虚拟机将新生代内存分为一块较大的 Eden 空间和两块较小的 Survivor(from,to) 空间(默认比例是 8:1:1),大多数情况下,分配对象时,大多数情况再Eden区域,当Eden空间被填满时,触发Minor GC,未被标记和回收的对象被移动到FromSpace中,同时ToSpace会清空,此时存活的对象年龄加一

经过多次Minor GC后,如果对象存活时间足够长(age > 15),则会被移动到老年代中。而老年代中的对象会在触发Major GC时被标记和清理。

如果虚拟机打开了 TLAB,那么对象优先在 TLAB 上分配。TLAB 全称是本地线程分配缓冲(Thread Local Allocation Buffer),它是每个线程在 Java 堆中预先分配的一小块内存。因为 TLAB是线程私有的,没有锁开销,因此性能较好,在 JDK7 之后默认开启。

大对象直接进入老年代

我们都知道大多数情况下对象都是在Eden区域分配的,但是如果一个对象是大对象,会直接分配在老年代。但是这个条件比较苛刻,需要使用Serial和ParNew垃圾回收器,并且需要设置-XX:PretenureSizeThreshold参数,这个参考默认是0,我们如果设置为4M,那么对象大于4M且使用Serial和ParNew垃圾回收器的话,这个对象就会直接进入老年代。

长期存活的对象进入老年代

大多数情况下,对象是在Eden区域分配,当Eden内存不够的情况下,进行一次Minor GC,未被标记和回收的对象被移动到FromSpace中,同时ToSpace会清空,此时存活的对象年龄加一。多次Minor GC后,如果对象存活时间足够长,age > 15的话,这个对象会被存入老年代。

age的大小可以根据下列参数来设置,但是最大是15,因为对象头里面存储对象自身的运行数据里面的GC分代年龄的是四个字节,最大是1111。

JVM垃圾回收_第2张图片

动态年龄判断

我们通过参数设置新生代的对象移动到老年代的年龄非常不好控制,如果是15的话,需要15次垃圾回收才进入,会一直在from to两个区域移来移去,当from或者to满了就会直接移动到老年代,我们设置的15没有生效,提前移进去了。如果设置太小,会过早的进入到老年代里面,没有在新生代里面被充分的回收掉。

我们会发现这个年龄不太靠谱。

JVM引入了动态年龄判断机制:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保

如果YougGC时新生代有大量对象存货下来,而survivor区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。

在执行每次YoungGC之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代YoungGC后,所有对象都存活下来了,而survivor区又放不下,那可能所有对象都要进入老年代了。

这个时候如果老年代的可用空间是大于新生代所有对象的总大小的,那就可以放心进行YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就可能老年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查-XX:HandlePromotionFailure参数是否允许担保失败。

如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次YoungGC,尽管这次YoungGC是有风险的。但是如果小于,或者-XX:HandlePromotionFailure参数不允许担保失败,这时就会进行一次Full GC。

在允许担保失败并尝试进行YoungGC后,可能会出现三种情况

(1)YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中。

(2)YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年代。

(3)YoungGC后,存活对象大于survivor大小,也大于老年大可用空间大小,老年代也放不下这些对象了,此时就会发生“Handle Promotion Failure”,就是触发了Full GC。如果Full GC后,老年代还是没有足够的空间,此时就会发生OOM内存溢出了。

总结

我们总结一下对象的分配过程,首先我们判断这个对象是否在栈上分配(JIT和逃逸分析),如果不在栈上分配,这个对象是否满足大对象的要求呢?满足就直接分配在老年代了,不满足的话看是否开启本地线程缓冲,开启对象分配在缓冲里面,没有的画分配在Eden里面。当进行一次Minor GC,Eden存活的对象移动到From区域,TO区域清空。多次发送Minor GC且年龄达到要求的对象会移动到老年代里面。有个特殊情况叫动态年龄判断,当From区域该年龄的对象达到From内存的一半或者以上,大于等于该年龄的对象直接进入老年代。

三.垃圾回收的基础算法

复制算法

JVM垃圾回收_第3张图片

复制算法中,把内存区域一分为二,进行垃圾回收的时候,把存活的对象复制到预留的位置,原来的位置里面的对象全部清楚,并且设置为预留区域。

特点:实现简单、运行高效;没有内存碎片;空间利用率只有一半。

Appel式的复制回收算法

JVM中使用的是Appel式的复制回收算法,这也是Eden,From,To区比例大小的由来。

JVM垃圾回收_第4张图片

因为分配出来的对象,绝大多数都是垃圾,所以没必要预留百分之五十。

特点:空间利用率比普通复制回收算法高。

标记清除算法

复制算法适用于新生代,因为新生代大部分都是垃圾,但是不适用于老年代,因为老年代经过多次垃圾回收的筛选,大部分不是垃圾。

JVM垃圾回收_第5张图片

标记清除算法把根不可达的标记一下,然后统一清理。

特点:位置不连续,产生内存碎片;可以做到不暂停(因为清除的都是垃圾对象,存活对象没有被移动)。

标记整理算法

JVM垃圾回收_第6张图片

标记整理算法一定是先标记,再整理,最后清除。因为先整理,用存活对象覆盖垃圾对象是没有问题的,整理好了后,再统一清楚垃圾对象即可,效率会高很多。

特点:没有内存碎片,指针需要移动。

对象移动了,线程一定要暂停的。因为对象移动了,真实的内存地址就会变化,而存在线程里面虚拟机帧里面的栈帧里面的局部变量表里面对这个对象的引用也要更新一下。

JVM垃圾回收_第7张图片

你可能感兴趣的:(JVM,jvm,java,开发语言,python,数据库,android)