1.概述
JVM虚拟机为什么要划分为四种引用类型,强引用(reference),软引用(soft reference),弱引用(weak reference),虚引用(phantom Reference)四种呢?
在最开始jdk1.2的时候,java其实只有强引用的,即在reference中只包含了一块内存的起始地址,代表了它是某个对象,某个地址方面的引用。但是,有一些额外的场景,需要我们创建一些生存周期不是那么长的对象,就比如缓存吧。这个就不好满足了,所以划分了如下四种。
2.引用类型
2.1 强引用
强引用,其实就是我们通过new创建出来的对象,这种创建出来的对象,引用直接就是用的Reference,它代表了,只要创建出来的对象强引用关系存在,那么,系统报出OOM异常,这个对象也不会被回收。
考虑一下,什么场景下,new对象被回收?它肯定不是创建了就不回收,假设在一个while循环很多次,创建某个对象,那一直不回收就过分了。简单举点例子:
User user = new User();
user = null,那么原先new出来的User对象与这个引用的引用关系去除了,这样肯定可以回收的。
我们不手动修改user = null,它是什么时候会回收呢?个人理解,之前在说引用计数法的时候,是从GC ROOT来开始往下遍历的,而GCROOT对象,局部变量这些也算。就像局部变量,都是保存在栈中的局部变量表中的。而栈是线程私有的,线程会结束的,有思路了码?
也就是说,线程挂掉,栈释放,局部变量无了,局部变量所引用的对象之间的强引用关系也无了,回收。
2.2 软引用
软引用,创建方式,依赖于SoftReference。它底层是一个泛型。
这种引用方式,和强引用有一点不用。当系统即将发生OOM异常的时候,此时软引用对象会被系统回收掉,优先保证系统的运行。
2.3 弱引用
软引用,创建方式,依赖于WeakReference。它底层是一个泛型。
这种引用方式,则是在软引用的基础上。只要系统发生了垃圾回收,弱引用对象必定会被回收。这块功能,其实就可以做缓存的。
2.4 虚引用
虚引用,,PhantomReference。最弱的引用关系,无法通过该引用获取导对象。设置虚引用的目的也只是在当前对象被垃圾回收器回收时收到一个系统通知。
这块,我记得还涉及导底层队列一些东西,但是当时没有看明白。后续再说吧。
3.测试代码以及垃圾收集日志
日志的话,我都在代码中注释中写过了,但该怎么说,其它的都好理解,软引用在OOM异常回收时这个日志没有看明白,后续看看。这些得配合分代年龄一块看。
package com.bo.jvmstudy.thirdchapter;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Set;
/**
* @Auther: zeroB
* @Date: 2022/9/4 09:49
* @Description: 四种引用类型,来模拟测试一下
*/
public class FourReferenceTest {
public static void main(String[] args) throws InterruptedException {
//reference();
softReference();
//weakReference();
}
private static void weakReference() {
/**
* [GC (System.gc()) [PSYoungGen: 4250K->488K(6144K)] 4250K->768K(19968K), 0.0007627 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (System.gc()) [PSYoungGen: 488K->0K(6144K)] [ParOldGen: 280K->709K(13824K)] 768K->709K(19968K), [Metaspace: 2957K->2957K(1056768K)], 0.0048614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
*/
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[2 * 1024 * 1024]);
System.gc();
}
private static void softReference() throws InterruptedException {
/**
* [GC (System.gc()) [PSYoungGen: 4250K->488K(6144K)] 4250K->2852K(19968K), 0.0012730 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (System.gc()) [PSYoungGen: 488K->0K(6144K)] [ParOldGen: 2364K->2757K(13824K)] 2852K->2757K(19968K), [Metaspace: 2957K->2957K(1056768K)], 0.0056127 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
*/
SoftReference<byte[]> softReference = new SoftReference<>(new byte[2 * 1024 * 1024]);
System.gc();
Thread.sleep(500);
System.out.println("----------------------第二次GC-----------------------");
/**
* ----------------------第二次GC-----------------------
* [GC (Allocation Failure) [PSYoungGen: 108K->128K(6144K)] 2865K->2885K(19968K), 0.0002827 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [GC (Allocation Failure) [PSYoungGen: 128K->128K(6144K)] 2885K->2885K(19968K), 0.0002378 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (Allocation Failure) [PSYoungGen: 128K->0K(6144K)] [ParOldGen: 2757K->2755K(13824K)] 2885K->2755K(19968K), [Metaspace: 2959K->2959K(1056768K)], 0.0050934 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
* [GC (Allocation Failure) [PSYoungGen: 0K->0K(6144K)] 2755K->2755K(19968K), 0.0002533 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(6144K)] [ParOldGen: 2755K->692K(13824K)] 2755K->692K(19968K), [Metaspace: 2959K->2959K(1056768K)], 0.0043828 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
*/
//分配20M内存,超过已有内存,看看会不会回收,看看系统GC日志
Byte[] tempBytes = new Byte[20 * 1024 * 1024];
}
private static void reference() {
/**
* [GC (System.gc()) [PSYoungGen: 4250K->488K(6144K)] 4250K->2824K(19968K), 0.0013431 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
* [Full GC (System.gc()) [PSYoungGen: 488K->0K(6144K)] [ParOldGen: 2336K->2757K(13824K)] 2824K->2757K(19968K), [Metaspace: 2957K->2957K(1056768K)], 0.0055947 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
*/
//2M
byte[] arr = new byte[2*1024*1024];
System.gc();
}
}
4.对象的存活或是死亡
一直都说垃圾回收对象,但垃圾回收是怎么回收对象的。根据可达性分析算法回收对象时,经历了什么?其实不是立刻回收的,底层经历了两层标记。
垃圾回收,就之前所说的,会通过GC ROOT引用链来往下走,然后找到不在引用链上对象,这个时候,对这批对象进行第一次标记。标记完成之后,查看虚拟机是否执行过这些对象的finalize方法,或者finalize方法被重写,如果执行过(不是第一次执行过了,待会写代码),或者没有被重写,则代表了接下来都不需要执行该方法。
如果该方法需要执行,那么这些需要执行的对象会放到F-Queue队列中,交给一个低优先级的Finalizer线程执行。只保证执行,不保证运行完成。垃圾回收期会对F-QUEUE中的对象进行第二次标记,如果finalize()方法第一次拯救了自己,则不用被回收,否则,就只能在第二次标记中被回收了。
5.对象两次标记的拯救代码
这种写法,工作中没有这么用过,虽然很简单,但是在考虑如果用其它方式来实现拯救这个过程,其实也懵了。意思很简单,第一次没有执行过finalize方法,所以执行拯救,在后面时,这个对象已经执行过finalize方法,就不另外执行了。
package com.bo.jvmstudy.thirdchapter;
/**
* @Auther: zeroB
* @Date: 2022/9/4 13:41
* @Description: JVM垃圾回收测试两次标记
*/
public class ObjectTwiceMarkedTest {
//定义这么一个属性,是为了调用finalize方法时将当前按对象赋值,防止被垃圾回收
public static ObjectTwiceMarkedTest markrd;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("GC Task is Running");
//拯救一下自己,注意markrd是静态变量
markrd = this;
}
public void isAlive() {
System.out.println("Object is alive");
}
/**
*
* @param args
* @throws InterruptedException
* 执行结果:GC Task is Running
* Object is alive
* dead
*/
public static void main(String[] args) throws InterruptedException {
markrd = new ObjectTwiceMarkedTest();
//垃圾回收
markrd = null;
System.gc();
//留出时间让它充分回收
Thread.sleep(500);
//观察marked是否被拯救
if (markrd != null) {
markrd.isAlive();
} else {
System.out.println("dead");
}
//其实就是看finalize方法是否执行过
markrd = null;
System.gc();
Thread.sleep(500);
if (markrd!= null) {
markrd.isAlive();
} else {
System.out.println("dead");
}
}
}