您的位置:首页 > 其它

六、终结处理 finalize() 与垃圾回收机制 GC

2019-01-08 23:58 337 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m0_37264516/article/details/86108495

finalize() 与析构函数

C++ 中由于没有垃圾回收器,所以,每个对象需要考虑被销毁的情况,当一个对象中包含有其他对象,在销毁这个对象之前需要先将其包含的其他对象销毁,再执行这个对象的销毁动作。

这里,销毁对象内部其他对象,或者在销毁当前对象前需要做的额外工作,都可以放在一个叫做析构函数的内部。

#include<iostream>
using namespace std;
class App {
int id;
public:
App(int i) {
cout << "构造函数" << endl;
id = i;
}
~App() {
cout<<"ID: "<<id<<"析构函数"<<endl;
};
};

int main() {
App t0(0);                     //栈中分配
App t1[3] {1,1,1};             //栈中分配,数组型对象

App *t2 = new App(2);          //堆中分配
delete t2;
App *t3 = new App[3] {3,3,3};  //堆中分配
delete []t3;
cout << "main函数结束,释放所有对象内存" << endl;
return 0;
}

输出结果及分析:
可以发现,不管是在栈中的对象还是在堆中 new 的对象,在被销毁时均会执行析构函数,并且开发者可以精确知道什么时候销毁对象。

回到 Java 中的

finalize()
函数,它的作用其实与 C++ 中的析构函数无异,同样是在对象被销毁前会执行这个函数。

问题的关键是 Java 中存在垃圾回收器,不需要开发者手动进行清除无用的对象,所以开发者并不知道垃圾回收器会在何时对这个对象进行清除(通常垃圾回收器以内存作为标准进行回收垃圾,当内存不足时,垃圾回收器便会执行清理操作,当内存充足时,即便有大量无用的对象,也不一定会执行清理操作)。

因此,我们不能以 C++ 中的析构函数的用法来使用

finalize()
函数,举个反例:

假设某个对象在创建时会往屏幕上绘制图案,如果不是明确地进行擦除,它可能永远都不会被擦除。
但是如果在 finalize 函数中加入擦除的动作,那么只有等到垃圾回收器回收这个对象时,才会被擦除,只要垃圾回收器一天没有回收它,那么它画在屏幕上的东西就存在一天。

finalize 的用途
我们知道,Java 中一切都是对象,而只要是对象均可以被垃圾回收器回收,那么,

finalize()
函数存在的意义是什么呢?

因为 Java 中可能存在采用 C 语言中类似的分配内存的做法,而不是用 Java 中常见的

new
,这种做法通常发生于调用本地方法的时候,本地方法只支持 C/C++,因此可能存在使用
malloc()
函数分配内存的方式,所以必须使用
free()
函数才能释放这部分内存。

总结上面的话就是

finalize()
函数通常用于释放 Java 调用 C/C++ 方法产生的对象的内存。实际开发中大部分情况不会用到它。

最后,如果希望在 Java 中进行除释放空间外的清理操作,需要明确调用某个 Java 方法(不能直接调用 finalize() 方法),这便等同于析构函数了,但却没有析构函数方便。

利用 finalize() 发现程序缺陷
下面代码中,模拟货物签收的场景,当未签收的货物被销毁时(货物都必须被签收),通过

finalize()
方法可以发现这个缺陷。

class Goods{
Boolean signIn = false;
Goods(Boolean signIn){
this.signIn = signIn;
}
void checkSignIn(){
this.signIn = true;
}
protected void finalize(){
if(!this.signIn){
System.out.println("ERROR:Goods is not sign in!");
}
}
}

public static void main(String [] args){
Goods goods = new Goods(false);
goods.checkSignIn();//货物签收

new Goods(true);//未签收的货物
System.gc();//强制执行垃圾回收
}
/*
Output:
ERROR:Goods is not sign in!
*/

垃圾回收机制

C++ 中,在堆上分配内存的代价是很高的,因此,初学者很可能以为 Java 中在堆上分配内存代价同样很高。但是实际上,Java 中的垃圾回收器可以提高对象的创建速度

首先来理解一下 C++ 中的堆,可以假设堆是一个院子,每个对象都在这个院子中,并且各自都管辖着自己的地盘,当某些对象被销毁后,必须要考虑这个空出来的地盘的重用,因此在堆上分配的代价主要在于对对象销毁后的地盘的重用(每次查找可用空间开销很大)。

在 Java 中的堆,更类似一个传送带,凡是有对象要被分配内存,那么传送带往前移动一格,Java 中的堆指针,则只需要移动到未分配的区域。因此效率与 C++ 中在堆栈上分配空间相当(尽管实际情况下在簿记工作方面有一定开销,但远远小于查找可用空间的开销)

此时,假设上面的堆模型中内存空间已经枯竭,那么即便“传送带”上存在空闲空间,也会导致内存耗尽的结局。

这个时候就需要垃圾回收器的介入了,它工作时,一边回收空间,一边将堆中的对象排列紧凑。

垃圾回收技术

一、引用计数
引用计数是一种简单,但速度很慢的垃圾回收技术。

原理是给每个对象一个引用计数器,当有引用连接至对象时,引用计数器加 1,当引用离开作用域或置为 null 时,引用计数器减 1,最后,垃圾回收器遍历所有对象,将引用计数器为0的对象销毁释放空间。

缺陷:当存在两个对象中均存在对方的引用,并且这两个对象无其他引用时,理论上它们都要被销毁,但它们的引用计数器不为 0。定位这种对象间存在循环引用的情况,所需工作量极大。

引用计数法常用于说明垃圾收集的工作方式。目前从未真正应用于任何一种虚拟机中。

二、引用链追溯法
这种技术的核心思想是:对于任何活的对象,一定可以追溯到堆栈或者静态区域中的引用。这个过程可能会穿过多个对象层次。

由此可知,从堆栈或者静态区域出发,遍历所有引用,就能找到所有活着的对象,当然,对于找到的对象中的其他对象的引用,同样要追溯。最后,所有访问过的对象都是活的,其他的都可以被销毁。

注意:这个方式可以完全解决交互自引用对象组的问题。

自适应垃圾回收技术

那么,该如何处理找到的这些活对象呢?

方法一:停止-复制(stop and copy),即先暂停程序(因此,它不是后台回收模式),将活对象复制到另一个堆,没有被复制的均为垃圾,在新堆中,对象与对象之间非常紧凑。

在复制到新堆后,所有指向新堆中的对象的引用都必须修正,包括堆栈或静态区域的引用,和对象内的引用(在遍历的过程中才能被找到并加以修正)。

缺陷:复制式回收器效率并不高,首先需要有两个堆,然后利用这两个堆来回倒腾。其次,程序进入稳定状态后可能产生的垃圾很少,甚至没有,但是复制式回收器依然会进行两个堆之间的复制。

方法二:标记-清扫(mark-and-sweep),这个方法同样是依据引用链追溯法扫描所有活着的对象,在扫描到活的对象时,将这个对象标记一下,扫描完成后,再将未标记的对象进行清理,这将导致堆空间是不连续的,如果希望得到连续的空间,则需要重新整理剩余对象。

这个方法一样需要暂停程序进行。

自适应垃圾回收技术就是 Java 虚拟机可以通过检查,如果没有新垃圾产生或者产生很少的垃圾,就会从停止-复制模式转换到标记-清扫模式

最后,上面所讨论的 Java 虚拟机中,内存均是以较大的

“块”
为单位的,每个块都有一个代数表示其是否存活。有了块的概念后,停止-复制方法就不必设置两个堆了,只要把存活的对象往废弃的块中复制即可。

大型的对象可能占用一整个块,对于大型对象,不会被复制,但是块的代数会增加,而内含小型对象的块则被复制并整理。

这对处理大量短命的临时对象很有帮助。

Java中其他用于提升速度的技术
即时编译技术(Just-In-Time)
这种技术可以把程序全部或部分翻译成机器码,这原本是 Java 虚拟机的工作,可以使得程序运行速度更快。

在程序运行时,首次加载一个类,首先需要找到它的 .class 字节码文件,此时可以选择让即时编译器编译所有代码,但是这样做的话,编译都分散在整个程序生命周期内,更加耗时,其次,编译后的机器码比字节码长。

惰性评估(lazy evaluation)
意思是即时编译器只会在必要时进行编译,这样一来,从不会被执行的代码可能压根不会被JIT编译。

例如 Java HotSpot 技术采用了类似的方法,代码每次执行都会进行优化,因此代码执行越多,速度越快

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: