Java Review (十八、面向对象----对象与垃圾回收)
@
Java运行时数据区域
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191103171234998.png?#pic_center)
而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,Java的垃圾回收机制所关注的是这部分内存。
Java垃圾回收机制具有如下特征 :
- 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接、网络IO等资源) 。
- 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存 。
- 在垃圾回收机制回收任何对象之前,总会先调用它的 finalize()方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象) ,从而导致垃圾回收机制取消回收。
对象在内存中的状态
当 一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:
- 可达状态 : 当 一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。
- 可恢复状态:如果程序中某个对象不再有任何引用变量引用它 ,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的 finalize()方法进行资源清理 。 如果系统在调用 finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。
- 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的 finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源 。
图二显示了对象的三种状态的转换示意图 。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191103152439209.png?#pic_center)
例如,下面程序简单地创建了两个字符串对象,并创建了 一个引用变量依次指向两个对象 。
public class StatusTranfer {
public static void test () {
String a = new String("马作的卢飞快"); //①
a = new String("弓如霹雳弦惊") ; //②
}
public static void main(String []args) {
test(); //③
}
}
-
当程序执行 test 方法的①代码时,代码定义了 一个 a 变量,并让该变量指向"马作的卢飞快"字符串,该代码执行结束后,"马作的卢飞快"字符串对象处于可达状态 。
-
当程序执行了 test 方法的②代码后,代码再次创建了"弓如霹雳弦惊"字符串对象,并让 a 变量指向该对象 。 此时,"马作的卢飞快"字符串对象处于可恢复状态,而"弓如霹雳弦惊"字符串对象处于可达状态。
一个对象可以被一个方法的局部变量引用, 也可以被其他类的类变量引用 ,或被其他对象的实例变量引用 。
- 当某个对象被其他类的类变量引用时, 只有该类被销毁后,该对象才会进入可恢复状态;
- 当某个对象被其他对象的实例变量引用时,只有当该对象被销毁后 ,该对象才会进入可恢复状态 。
强制垃圾回收
当一个对象失去引用后,系统何时调用它的 finalize()方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,但不能控制它何时被回收 。
虽然程序无法精确控制 Java 垃圾回收的时机,但可以强制系统进行垃圾回收一一这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定 。 大部分时候,程序强制系统垃圾回收后是有效的。
强制系统垃圾回收有如下两种方式 :
- 调用 System 类的 gc()静态方法: System.gc()
- 调用 Runtime 对象的 gc()实例方法: Runtime.getRuntime().gc()
下面程序创建了 4 个匿名对象 , 每个对象创建之后立即进入可恢复状态,等待系统回收 , 但直到程序退出,系统依然不会回收该资源 。
public class GcTest {
public static void main(String[] args) {
for (int i = 0 ; i < 4 ; i++) {
new GcTest();
}
}
public void finalize () {
System.out.println (" 系统正在清理 GcTest 对象的资源 . . . ") ;
}
}
编译、运行上面程序 , 看不到任何输出,可见直到系统退出 , 系统都不曾调用 GcTest 对象的 finalize()方法 。 但如果将程序修改成如下形式 :
public class GcTest {
public static void main(String[] args) {
for (int i = 0 ; i < 4 ; i++) {
new GcTest();
// 下面两行代码的作用 完全相同,强制系统进行垃圾回收
//System.gc() ;
Runtime.getRuntime() . gc();
}
}
public void finalize () {
System.out.println (" 系统正在清理 GcTest 对象的资源 . . . ") ;
}
}
上面程序与前一个程序相比,增加了强制系统进行垃圾回收代码 。 编译上面程序,使用如下命令来运行此程序 :
java -verbose:gc GcTest
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191103160805904.png?#pic_center)
运行 Java 命令时指定 -verbose :gc 选项,可以看到每次垃坡回收后的提示信息,如图三所示 。
从图三 中可以看出,每次调用了 Runtime.getRuntime(). gc()代码后 , 系统垃圾回收机制 还是"有所动作"的,可以看出垃圾回收之前、回收之后的内存占用对 比 。
虽然图三显示了程序强制垃圾回收的效果,但仍然要认识到这种强制只是建议系统立即进行垃坡回收 , 系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理 : 垃圾回收机制会在收到通知后,尽快进行垃圾回收 。
finalize 方法
在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源 , 在没有明确指定清理资源的情况下, Java 提供了默认机制来清理该对象的资源,这个机制就是 finalize()方法 。
该方法是定义在 Object 类里的实例方法,方法原型为 :
protected void finalize() throws Throwable
当 finalize()方法返回后,对象消失,垃圾回收机制开始执行 。方法原型中的 throws Throwable 表示它可以抛出任何类型的异常 。
任何 Java 类都可以重写 Object 类的 finalizeO方法,在该方法中清理该对象占用的资源 。 如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的 finalize()方法来清理资源 。
垃圾回收机制何时调用对象的 finalizeO方法是完全透明的,只有当程序认为需要更多的额外内存时,垃圾回收机制才会进行垃坡回收 。 因此,完全有可能出现这样一种情形:某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的自finalize()方法也不会得到调用。
finalize()方法具有如下 4 个特点 :
- 永远不要主动调用某个对象的 finalize()方法,该方法应交给垃圾回收机制调用 。
- finalize()方法何时被调用,是否被调用具有不确定性 ,不要把自finalize()方法当成一定会被执行的方法 。
- 当 JVM 执行可恢复对象的 finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。
- 当执行 finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行 。
下面程序演示了如何在 finalize()方法里复活自身,并可通过该程序看出垃圾回收的不确定性。
public class FinalizeTest {
private static FinalizeTest ft = null;
public void info() {
System.out.println( "测试资源清理的 finalize 方法 " );
}
public static void main(String[] args) throws Exception{
// 创建 FinalizeTest 对象立即进入可恢复状态
new FinalizeTest() ;
// 通知系统进行资源回收
System.gc(); //①
// 强制垃圾回收机制调用可恢复对象的 finalize ()方法
// Runtime.getRuntime() . runFinalization() ; //②
System.runFinalization(); //③
ft.info();
}
public void finalize() {
// 让 ft 引用到试图回收的可恢复对象,即可恢复对象重新变成可达
ft = this;
}
}
上面程序中定义了 一个 FinalizeTest 类,重写了该类的 finalizeO方法,在该方法中把需要清理的可恢复对象重新赋给 a 引用变量,从而让该可恢复对象重新变成可达状态。
上面程序中的 main()方法创建了 一个 FinalizeTest 类的匿名对象,因为创建后没有把这个对象赋给任何引用变量,所以该对象立即进入可恢复状态 。 进入可恢复状态后:
- 系统调用①号字代码通知系统进行垃圾回收
- ②号代码强制系统立即调用可恢复对象的 finalize()方法,再次调用位对象的 info()方法 。编译、运行上面程序 , 看到 info()方法被正常执行 。
- 如果删除①行代码,取消强制垃圾回收 。 再次编译、运行上面程序,将会看到如图四所示的结果 。
图四:调用info()方法时引发空指针异常
从图四所示的运行结果可以看 出,如果取消①号代码,程序并没有通知系统开始执行垃圾回收(而且程序内存也没有紧张 ) ,因此系统通常不会立即进行垃圾回收,也就不会调用 FinalizeTest对象的 fmalize()方法,这样 FinalizeTest 的ft类变量将依然保持为 null,这样就导致了空指针异常 。
上面程序中②号代码和③号代码都用于强制垃圾回收机制调用可恢复对象的 finalize()方法,如果程序仅执行 System.gc(); 代码,而不执行②号或③号代码一一由于 JVM垃圾回收机制的不确定性,JVM往往并不立即调用可恢复对象的 finalize()方法,这样 FinalizeTest 的ft类变量可能依然为 null ,可能依然会导致空指针异常。
对象的软、弱和虚引用
对大部分对象而言 ,程序里会有一个引用变量引用该对象,这是最常见的引用方式 。 除此之外,还有软引用、弱引用、虚引用。
1. 强引用 (StrongReference)
强引用就是指在程序代码之中普遍存在的,类似 "Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
2. 软引用 (SoftReference)
软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了 SoftReference类来实现软引用。
3. 弱引用 (WeakReference)
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用 关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当 前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供WeakReference类来实现弱引用。
4. 虚引用 (PhantomReference)
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚 引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实 例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一 个系统通知。在JDK 1.2之后,提供了 PhantomRcfcrcncc类来实现虚引用。
上面三个引用类都包含了 一个 get()方法,用于获取被它们所引用的对象 。
引用队列由 java. lang.ref.ReferenceQueue 类表示,它用于保存被回收后对象的引用 。 当联合使用软引用、弱引用和引用队列时,系统在回收被引用的对象之后,将把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它关联的引用队列中,这使得可以在对象被回收之前采取行动 。
软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含了该虚引用,从而了解虚引用所引用的对象是否即将被回收 。
下面程序示范了弱引用所引用的对象被系统垃圾回收过程 :
public class ReferenceTest {
public static void main(String[] args) throws Exception {
//创建一个字符串对象
String str = new String( "疯狂Java讲义 " ) ;
//创建一个弱引用,让此弱引用引用到 " 疯狂 Java 讲义 " 字符串
WeakReference wr = new WeakReference(str) ; //①
//切断 str 引用和 "疯狂 Java 讲义 " 字符串之间的引用
str = null; // ②
//取出弱引用所引用的对象
System.out.println(wr.get()); //③
//强制垃圾回收
System.gc ();
System.runFinalization ();
//再次取出弱引用所引用的对象
System.out.println(wr.get()); //④
}
}
上面程序先创建了 一个"疯狂 Java 讲义"宇符串对象,并让 5位引用变量引用它,执行①行粗体字代码时,系统创建了 一个弱引用对象,并让该对象和 str引用同一个对象 。 当程序执行到②行代码时,程序切断了str和 "疯狂 Java 讲义 "字符串对象之间 的引用关系 。 此时系统内存如图 6.10 所示 。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191103163149363.png?#pic_center)
当程序执行到③号代码时 ,由于本程序不会导致内存紧张 ,此时程序通常还不会回收弱引用wr 所引用的对象,因此在③号代码处可以看到输出 "疯狂 Java 讲义"字符串 。执行到③号代码之后,程序调用了 System.gc(); 和 System.runFinalization(); 通知系统进行垃圾回收,如果系统立即进行垃圾回收,那么就会将弱引用 WT 所引用的对象回收 。 接下来在④号字代码处将看到输出 nul l 。
下面程序与上面程序基本相似,只是使用了虚引用来引用字符串对象,虚引用无法获取它引用的对象。下面程序还将虚引用和引用队列结合使用,可以看到被虚引用所引用的对象被垃圾回收后,虚引用将被添加到引用队列中 。
public class PhantomReferenceTest {
public static void main(String[] args) throws Exception{
//创建一个字符串对象
String str = new String("疯狂 Java 讲义 " ) ;
//创建一个引用队列
ReferenceQueue rq = new ReferenceQueue();
// 创建一个虚引用,让此虚引用引用到"疯狂 Java 讲义"字符串
PhantomReference pr = new PhantomReference (str , rq);
// 切断 str 引用和 " 疯狂 Java 讲义 " 字符串之间的引用
str = null;
// 取出虚 引用所引用的对象,并不能通过虚引用获取被引用的对象,所以此处输出 null
System. out .println(pr . get()) ; // ①
// 强制垃圾回收
System . gc () ;
System . runFinalization();
//垃圾回收之后 ,虚引用将被放入引用队列中
// 取出引用 队列中最先进入队列的引用与 pr 进行比较
System. out.println(rq.poll()==pr); // ②
}
}
因为系统无法通过虚引用来获得被引用的对象,所以执行①处的输出语句时,程序将输出 null (即使此时并未强制进行垃圾回收)。当程序强制垃圾回收后,只有虚引用引用的字符串对象将会被垃圾回收,当被引用的对象被回收后,对应的虚引用将被添加到关联的 引用队列中,因而将在②代码处看到输出 true 。
使用这些引用类可以避免在程序执行期间将对象留在内存中。如果以软引用、弱引用或虚引用的方式引用对象,垃圾回收器就能够随意地释放对象 。如果希望尽可能减小程序在其生命周期中所占用的内存大小时,这些引用类就很有用处。
必须指出:要使用这些特殊的引用类,就不能保留对对象的强引用:如果保留了对对象的强引用,就会浪费这些引用类所提供的任何好处 。
由于垃圾回收的不确定性,当程序希望从软、弱引用中取出被引用对象时,可能这个被引用对象己经被释放了。如果程序需要使用那个被引用的对象,则必须重新创建该对象 。 这个过程可以采用两种方式完成,下面代码显示了其中一种方式。
// 取出弱引用所引用的对象
obj = wr.get() ;
//如果取出的对象为 null
if (obj == null){
// 重新创建一个新的对象,再次让弱引用去引用该对象
wr = new WeakReference(recreatelt()) ; // ①
//取出弱引用所引用的对象,将其赋给 obj 变量
bj = wr. get () ; // ②
}
// 操作 obj 对象
// 再次切断 obj 和对象之间的关联
obj =null ;
下面代码显示了另 一种取出被引用对象的方式 。
// 取出弱引用所引用的对象
obj = wr.get();
// 如果取出的对象为 null
if (obj == null){
//重新创建一个新的对象,并使用强引用来引用它
obj = recreatelt();
// 取出弱引用所引用的对象,将其赋给 obj 变量
wr = new WeakReference(obj);
}
//操作 obj 对象
//再次切断。同和对象之间的关联
obj = null;
上面两段代码采用的都是伪码,其中 recreatelt()方法用于生成一个 obj 对象 。 这两段代码都是先判断 obj 对象是否已经被回收 ,如果己经被回收,则重新创建该对象 。 如果弱引用引用的对象己经被垃圾回收释放了,则重新创建该对象 。 但第一段代码存在一定的问题:当 if 块执行完成后, obj 还是有可为 null 。 因为垃圾回收的不确定性,假设系统在①和②行代码之间进行垃坡回收,则系统会再次将 wr所引用的对象回收,从而导致 obj 依然为 null。第二段代码则不会存在这个问题,当 if 块执行结束后,obj 一 定不为 null 。
参考:
【1】:《疯狂Java讲义》
【2】:《深入理解Java虚拟机:JVM高级特性与最佳实践》
【3】:https://www.cnblogs.com/czwbig/p/11127124.html
【4】:https://www.cnblogs.com/czwbig/p/11127159.html