前言: 在程序运行中会使用到各个类的对象实例来进行编程,我们知道实例化的对象都放入到堆中,并产生一个引用指向堆中的这个对象;Jvm怎么通过引用找到某个对象;这个对象什么时候产生,又是什么时候消亡呢;jvm又是以什么方式来判定改对象是否需要回收呢;
本文以以下几个方面来讨论对象的引用及生存周期:
1 jvm 中对象的引用;
2 对象的生命周期;
3 对象的不可达判定;
1 jvm 中对象的引用:
引用定位到对象的方式有两种,一种叫句柄池访问,一种叫直接访问
句柄池:
使用句柄访问对象,会在堆中开辟一块内存作为句柄池,句柄中储存了对象实例数据(属性值结构体) 的内存地址,访问类型数据的内存地址(类信息,方法类型信息),对象实例数据一般也在heap中开辟,类型数据一般储存在方法区中。
优点:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为) 时只会改变句柄中的实例数据指针,而reference本身不需要改变。
缺点:增加了一次指针定位的时间开销。
直接访问:
直接指针访问方式指reference中直接储存对象在heap中的内存地址,但对应的类型数据访问地址需要在实例中存储。
优点:节省了一次指针定位的开销。
缺点:在对象被移动时(如进行GC后的内存重新排列),reference本身需要被修改。
Java 中引用使用的是直接访问;
2 对象的生命周期:
程序在运行过程中不断的创建和使用对象,那么一个类的实例对象从出生到消亡的过程又是怎么样呢?
2.1创建阶段:
程序创建类实例对象,或者jvm 在加载类是创建类对象;
- 为对象分配存储空间
- 开始构造对象
- 从超类到子类对static成员进行初始化
- 超类成员变量按顺序初始化,递归调用超类的构造方法
- 子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
2.2 应用阶段( In Use ) :
对象至少被一个强引用持有着。
补充:对象引用的分类:
2.2.1 强引用(StrongReference):
在 Java 中最常见的就是强引用,也是我们在开发过程中经常会使用到的引用.把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收(如果引用一直存在)。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。因此强引用是造成 Java 内存泄漏的主要原因之 一。
2.2.2 软引用 (SoftRefernce)
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它 不会被回收,当系统内存空间不足时它会被回收(中间的gc不会被清除)。软引用通常用在对内存敏感的程序中。引用的对象会在系统将要发生内存溢出之前,被列入垃圾回收的范围进入回收。
public class SoftReferenceDemo {
public static void main(String[] args) {
//。。。一堆业务代码
Object a = new Object();
//。。业务代码使用到了我们的Worker实例
// 使用完了a,将它设置为soft 引用类型,并且释放强引用;
SoftReference sr = new SoftReference(a);
a = null;
//
// 下次使用时
if (sr != null) {
a = (Object) sr.get();
} else {
// GC由于内存资源不足,可能系统已回收了a的软引用,
// 因此需要重新装载。
a = new Object();
sr = new SoftReference(a);
}
}
}
2.2.3 弱引用(WeakReference):
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//100M的缓存数据
byte[] cacheData = new byte[100 * 1024 * 1024];
// 将缓存数据用软引用持有
WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
System.out.println("第一次GC前" + cacheData);
System.out.println("第一次GC前" + cacheRef.get());
//进行一次GC后查看对象的回收情况
System.gc();
// 等待GC
Thread.sleep(500);
System.out.println("第一次GC后" + cacheData);
System.out.println("第一次GC后" + cacheRef.get());
//将缓存数据的强引用去除
cacheData = null;
System.gc();
//等待GC
Thread.sleep(500);
System.out.println("第二次GC后" + cacheData);
System.out.println("第二次GC后" + cacheRef.get());
}
}
第一次GC前[B@619a5dff
第一次GC前[B@619a5dff
第一次GC后[B@619a5dff
第一次GC后[B@619a5dff
第二次GC后null
第二次GC后null
2.2.4 虚引用(PhantomReference):
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收;
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
2.3 不可见阶段( Invisible ) :
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。 简单说就是程序的执行已经超出了该对象的作用域了。
2.4 不可达阶段( Unreachable ) :
对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为” GC root ”。存在着这些 GC root 会导致对象的内存泄露情况,无法被回收。
处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。
2.5 收集阶段( Collected ) :
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了 finalize() 方法,则会去执行该方法的终端操作。
注意:不要重载finazlie()方法!原因有两点:
-
会影响JVM的对象分配与回收速度
在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。 -
可能造成该对象的再次“复活”
在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
2.6 终结阶段 :
当对象执行完fifinalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
2.7 :对象空间重新分配阶段
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。
3 对象的不可达判定:
既然不可达的对象会被jvm 去进行回收,那么哪些对象是不可达的,哪些对象是可达的?
3.1 对象的可达性分析:
引用计数法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被再使用的。主流的JVM里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象间的互循环引用的问题;
gc root 可达(java 采用此计算方法):
这个算法的基本思想是通过一系列被称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径叫做引用链,当一个对象到GC Roots没有任何引用链相连 (用图论的话来说就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
Java语言中,哪些可作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
即使在可达性分析法中不可达的对象,也并非“非死不可”,他们还有拯救自己的机会。要宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后没有与GC Roots的引用链,那么它将会被第一次标记,并且此时需要判断是否有必要执行finalize()方法。没有必要的话,那么这个对象就宣告死亡,可以回收了。
如果有必要执行,那么这个对象会被放置在一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立的低优先级的Finalizer线程去执行它。finalize()是对象拯救自己的最后一次机会-只要重新与引用链上的 任何一个对象建立关联即可(譬如把自己赋值给某个类变量或者对象的成员变量),那么在第二次标记时它将被移除“可回收”的集合,如果对象还没有逃脱,基本上就真的被回收了。