文章目录
关于对象能否被回收:
- 计数器
- 可达性分析
还可以根据引用的类型,不同的引用类型,对应对象的不同GC回收规则。
0、相关🖊
1、强引用
- 默认强引用,即把一个对象赋值给一个变量(也叫引用)
java
Object o = new Object();
- GC时,有强引用的对象不会被回收,即使OOM了
Demo:
java
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println("GC前: " + demo);
System.gc();
System.out.println("GC后: " + demo);
//断掉强引用
demo = null;
System.gc();
System.out.println("断掉强引用并GC: " + demo);
}
}
2、软引用
- 内存足够时,不会被GC回收
- 内存不足时,才被GC回收
- 包装为软引用:
new SoftReference<对象类型>(对象)
java
public class SoftReferenceDemo {
public static void main(String[] args) {
byte[] byte1 = new byte[1024 * 1024 * 100];
SoftReference<byte[]> softReference = new SoftReference<>(byte1);
byte1 = null;
System.gc();
System.out.println("内存充足时:" + softReference.get());
try {
byte[] bytes = new byte[1024 * 1024 * 100];
} catch (Error e) {
e.printStackTrace();
} finally {
System.out.println("内存不足时:" + softReference.get());
}
}
}
java
public class SoftReferenceDemo {
public static void main(String[] args) {
SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 100]);
System.gc();
System.out.println("内存充足时:" + softReference.get());
try {
byte[] bytes = new byte[1024 * 1024 * 100];
} catch (Error e) {
e.printStackTrace();
} finally {
System.out.println("内存不足时:" + softReference.get());
}
}
}
注意上面两份代码的区别,前者必须加个byte1 = null,这是个强引用,不断掉,即使内存不够,byte1对象也不会被回收,soft Reference.get结果也就一直不为null。这个地方卡了半小时,想着怎么还不回收,看半天发现这儿有个强引用。设置-Xmx200m,运行:
以上代码,盒子里的东西已经没了(被包装的对象被回收,get得到结果为null了),盒子也就没必要再留了。 但盒子里的东西何时被回收不确定,不能直接写一句先把盒子干掉:
java
softReference = null;
软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收:
- 创建软引用时,构造方法里再传入一个引用队列
- 对象A被回收,外层的SoftReference对象会加入队列
- 遍历干掉外层的SoftReference
Demo:
java
public class SoftReferenceDemo {
public static void main(String[] args) {
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
byte[] byte1 = new byte[1024 * 1024 * 100];
//包装对象时再传个队列
SoftReference<byte[]> softReference = new SoftReference<>(byte1, queue);
list.add(softReference);
}
int count = 0;
//能从队里拿出来的,都是对象被回收的
while (queue.poll() != null) {
++count;
}
System.out.println(count);
}
}
设置JVM堆内存total和max为200M(实际可用约190M左右),循环十次,自然有九个byte[ ] 对象回收,queue长度应该是9:
poll弹出的就是被回收掉内存对象的SoftReference对象。
3、弱引用
- 不管JVM内存是否够用,GC运行,弱引用对象均被回收。
- 和软引用一样,也可搭配一个引用队列
- 用于ThreadLocal应对内存泄漏
java
public class Demo {
public static void main(String[] args) {
WeakReference<Demo> reference = new WeakReference<>(new Demo());
System.out.println("GC前: " + reference.get());
System.gc();
System.out.println("GC后: " + reference.get());
}
}
4、虚引用
- 幽灵引用/幻影引用
- 虚,形同虚设的意思
- 和其他几种引用不一样,它不影响对象的回收规则
- 仅有虚引用指向的对象,随时可能会被回收
- 唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知
- 虚引用get方法返回结果总为null
虚引用的一个应用场景是直接内存的释放问题:
java
public class Demo {
public static final int size = 1024 * 1024 * 10;
public static void main(String[] args) {
/**
* allocateDirect方法创建DirectByteBuffer对象
* DirectByteBuffer对象构造方法里向操作系统申请了直接内存
*/
ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
//干掉强引用
directBuffer = null;
System.gc();
System.out.println();
}
}
DirectByteBuffer对象被回收的时候,需要收到一个消息,去把直接内存的空间也释放了(不能只GC把堆里的DirectByteBuffer对象空间释放了,GC主要是处理堆,不是处理直接内存的)
往下跟:
Cleaner类继承了虚引用类,这里传入要监控的ByteBuffer对象,告诉虚引用我要监控这个对象的回收,接下来会有一个线程去监控这个对象的回收,
当ByteBuffer对象被回收,就调用Deallocator类(实现了Runnable接口)的run方法,run方法里干的活儿就是释放了直接内存:
贴个清晰点的Demo:
java
public class ReferenceDemo {
public static void main(String[] args) {
MyObject myObject = new MyObject();
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject, referenceQueue);
List<byte[]> list = new ArrayList<>();
new Thread(() -> {
while (true){
list.add(new byte[1024 * 1024]); //1M
//歇500ms,写1M进List
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//验证下每次get都是null
System.out.println(phantomReference.get() + " list add OK.");
}
},"t1").start();
new Thread(() -> {
while (true){
Reference<? extends MyObject> reference = referenceQueue.poll();
if(reference != null){
System.out.println("有虚引用对象被回收,加入了队列");
//break;
}
}
},"t2").start();
}
}
5、终结引用
- 对象需要被回收时,终结器引用会关联这个对象,并放入Finalizer类的引用队列
- (无需手动编码,其内部配合引用队列使用)
- 稍后会由FinalizerThread线程从队列中获取这个对象,并执行它的finalize方法
- 在这个对象该第二次被回收时,才真正干掉这个对象
总结就是第一次包装一下扔到引用队列+执行finalize方法,第二次GC它时,抬走。根据这个特点,如果在第三步里的finalize方法里给变为null的对象,重新赋一个强引用,岂不是可以让这个对象复活。 Demo:
java
public class FinalizeReferenceDemo {
public static FinalizeReferenceDemo reference = null;
/**
* 存活性验证
*/
public void alive() {
System.out.println("当前对象还存活...");
}
@Override
protected void finalize() throws Throwable {
try {
System.out.println("finalize方法执行===");
//设置强引用自救
reference = this;
} finally {
super.finalize();
}
}
@SneakyThrows
public static void test() {
reference = null;
System.gc();
//执行finalize方法的优先级低,这里等一会儿再往下走
Thread.sleep(500);
if (reference != null) { //若上面finalize方法执行,则这里不会为null了
reference.alive();
} else System.out.println("对象已被回收!");
}
public static void main(String[] args) {
reference = new FinalizeReferenceDemo();
test();
test();
}
}
运行结果:
test方法第一次调用,对象引用被置为null,并手动GC,该被回收了,此时进入引用队列并在稍后执行finalize方法。重写的finalize方法里给引用重新赋值,不为null了,test方法调alive方法发现对象又活了。
接着再第二次调test方法,按理说和第一次调test方法是一个流程,但finalize方法源码有说明:
即finalize方法最多被同一个JVM调用调用一次,对于一个被放弃的对象。所以第二次调test把引用又置为null并GC后,不会再调finalize方法,因此休眠500ms后,引用依然为null,对象被回收。