如何判断对象已死?
在堆中存放着程序中几乎所有的对象实例,垃圾收集器在对堆进行内存回收之前(消灭已死的对象),必须要判断哪些对象的引用已经失效了("已死")。
引用计数法(Reference Counting)
算法逻辑:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的(已死)
优点:判定效率很高,原理简单(python采用的就是引用计数法)
缺点:无法判断循环引用的对象是否已死亡,当存在多个对象相互引用时,此时的计数器始终大于0,引用计数法无法清理循环引用垃圾
可达性分析算法(Reachability Analysis)
可达性分析算法也叫根搜索算法:可达性分析算法会将 根对象 命名为 GC Root (根对象有多个),以GC Roots集合中的GC Root作为起点,根据引用关系向下搜索,搜索过程 所走过的路径 称为 引用链 (Reference Chain),如果某个对象到GC Root间没有任何引用链(即 **GC
Root与这个对象不可达** ),则标记这个对象为 不可达对象 ,此时执行 finalize()方法时,如果依旧无法复活,则第二次GC将会对其进行回收
可达性算法何时执行finalize()方法
在可达性算法中判定为不可达对象,即这个对象处于死/没死的中间状态(薛定谔的对象),必须经历第二次标记才能确定对象已失效
二次标记过程:
1、 如果对象在进行可达性分析后,发现某个对象并没有与GC Root相连接的引用链,那么它会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法
2、 对象没有重写finalize()方法
finalize()方法已经被虚拟机调用过
这两种情况都会被虚拟机视为没有必要执行finalize()方法
3、 如果这个对象被判定为有必要执行finalize()方法,那么该对象会被放置在一个名为F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法
虚拟机只会触发finalize()的开始,并不会等待它运行结束(以免执行缓慢或死循环导致F-Queue队列中的其他对象永久等待,最终会导致整个内存回收子系统崩溃)
4、 执行完finalize()方法后就会进行第二次垃圾回收,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,就可以不被垃圾收集器回收
/**
* @author caihuaxin
* @version 1.0.0
* @describe 此代码演示了两点:
* 1. 对象可以在被GC时自我拯救
* 2. 自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @date 2023/12/18
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes!I am still isAlive.");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK!= null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no!I am dead.");
}
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK!= null) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no!I am dead.");
}
}
}
Java技术体系中,固定可作为GC Root的对象包括以下几种
除了这些固定的GC Root集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性地加入,共同构成完整的GC Roots
例如:分代收集和局部回收都是针对堆中某一块区域发起的垃圾收集,但是某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,因此这些关联区域的对象也会一并加入GC Roots集合中
在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
方法区中类静态属性引用的对象(静态引用),譬如Java类的引用类型静态变量
在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
在本地方法栈中JNI(即Native方法)引用的对象
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointException、OutOfMemoryError)等,还有系统类加载器
所有被同步锁(synchronized关键字)持有的对象
反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
注意事项:
- 并不鼓励重写finalize()方法来复活对象,应该尽量避免
- finalize()方法运行代价高、不确定性大、无法保证各个对象的调用顺序、可能导致性能问题
- JDK9已经将finalize()方法标记为已过时,引入java.lang.ref.Cleaner类和java.lang.ref.PhantomReference类来代替finalize()
- Cleaner类的使用(推荐)
import java.lang.ref.Cleaner;
/**
* @author caihuaxin
* @version 1.0.0
*/
public class MyResource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final ResourceCleaner resourceCleaner;
private final Cleaner.Cleanable cleanable;
public MyResource() {
this.resourceCleaner = new ResourceCleaner();
// 注册资源清理器,当调用clean()方法时,会执行资源清理器的run方法
this.cleanable = cleaner.register(this, resourceCleaner);
}
private static class ResourceCleaner implements Runnable{
@Override
public void run() {
System.out.println("Clean up the resouce");
}
}
@Override
public void close(){
cleanable.clean();
}
}
public static void main(String[] args) {
try(MyResource myResource = new MyResource()){
System.out.println(1/0);
}catch (Exception e){
System.out.println("异常");
}
}
Clean up the resouce
异常
PhantomReference(虚引用)类的使用(需要控制清理时机的情况下推荐)
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
* @author caihuaxin
* @version 1.0.0
* @describe 虚引用触发对象释放
*/
public class PhantomReferenceExample {
public static void main(String[] args) {
//创建一个队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
//创建一个对象
Object myObject = new Object();
//将资源和队列都放入PhantomReference中
PhantomReference<Object> phantomReference = new PhantomReference<>(myObject, referenceQueue);
// GC对象回收线程检查引用队列
Thread cleanerThread = new Thread(() -> {
try {
// remove() 是阻塞方法,将等待直到有元素被加入队列
PhantomReference<Object> ref = (PhantomReference<Object>) referenceQueue.remove();
// 当ref被加入队列时,表示关联的PhantomReference所引用的对象已被垃圾回收
performCleanup(ref);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
cleanerThread.start();
// 清理对象,使其变为不可达状态(正常情况下不会这么做,只是为了示例)
myObject = null;
// 提示JVM进行垃圾收集,实际使用中不应该手动调用
System.gc();
}
private static void performCleanup(PhantomReference<Object> ref) {
// 在这里执行清理的具体动作
System.out.println("Cleaning up resources associated with the phantom reference.");
}
}
Cleaning up resources associated with the phantom reference.
总而言之,目前Java更推荐使用try-with-resource语句来自动管理资源,这样可以保证无论是否出现异常,都能保证资源的及时释放。 对于那些无法保证用户手动关闭资源的情况,就可以使用Cleaner或者PhantomReference来回收资源。
引用的概念
JDK1.2之前:如果该引用类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该引用数据是代表某块内存、某个对象的引用 我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,就可以抛弃这些对象(例如系统的缓存功能) JDK1.2之后:将引用分为了强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)
- 强引用:是指在程序代码之中普遍存在的引用赋值,即类似Object obj = new Object()这种引用关系,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 软引用:是指在程序中还有用,但非必须的对象。只被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围之中进行第二次回收(如果第二次回收依旧无法释放足够的内存,则会抛出内存溢出的异常),SoftReference类来实现软引用。
- 弱引用:也是指在程序中非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能够生存到下一次垃圾收集发生为止(无论内存是否足够,都会在第二次垃圾回收时回收弱引用对象)。WeakReference类来实现弱引用。
- 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。虚引用存在的目的只是为了能在这个对象被垃圾收集器回收时收到一个系统通知。PhantomReference类来实现虚引用。