内容简介:Java 的垃圾回收策略
当程序 创建对象,数组 等 引用类型实体 时,系统都会在 堆内存 中为之分配一块内存区,对象就保存在这块内存区,当这块内存不再被任何变量引用时,这块内存就变成垃圾,系统就要回收。
- 只回收堆内存中对象,不会回收物理资源
- 程序无法精确控制回收时机。
- 在垃圾回收机制回收任何对象之前,总会先调用它的 finlize() 方法,可能导致 垃圾回收机制取消 。
如何判断对象是否已经死亡?
引用计数算法
古老的判断对象是否存活的算法是:给对象添加一个计算器,一旦有地方引用该对象,则计数器加一,当引用失效时,就减一。任何计数器为 0 的对象,就不能再使用了。这种算法虽然经典,但是其并不能解决一个对象之间相互循环引用的问题。
public class RefreenceCount{ class GcObject{ public Object instance = null; } public static void main(String[] args){ GcObject o1 = new GcObject(); GcObject o2 = new GcObject(); o1.instance = o2; // 1 o2.instance = o1; // 2 o1 = null; o2 = null; } }
如上我们在最后注释一和注释二处还是无法释放对方。这样会造成堆溢出。
可达性分析算法
主流的语言中都是通过可达性算法分析对象是否存活的:通过一系列称为 “GC Roots” 对象作为起始点,从这些节点开始向下搜索,其所走过的路径称为引用链,当一个对象到 GC Roots 不可达时,则该对象是不可用的,也就是判断这些对象为可回收的。
在 Java 语言中,如下可作为 GC Roots 对象:
- 虚拟机栈中引用的本地对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
而可达性算法,我们可以看一个非常好的示例。 垃圾回收机制中,引用计数法是如何维护所有对象引用的
再谈引用
前面所说的两种都是跟引用有关系。在 JDK1.2 之前,引用的定义很狭隘,一个对象只有被引用和未被引用的两种状态。在 JDK1.2 之后,对引用的定义进行了扩充,我们希望:当内存空间还足够时候,则对象能够保留在内存中;如果内存空间在进行垃圾回收之后,内存空间还是非常紧张,则抛弃这些对象。适合于很多缓存功能的应用场景。分成四种强度递减的引用:
-
强引用:这种引用很常见,类似于 :
A a = new A()// 强引用
只要引用存在,就不会回收引用对象。如果显示的将对象置为 null 或者其超出对象的生命范围,则被回收。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题
-
软引用:如果一个对象具有软引用,内存足够时,gc不会回收它;内存不足时,gc就会回收这个对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可用来实现内存敏感的高速缓存。
-
弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否,都会回收它的内存 。
public class WeakRefrenceDemo { public static void main(String[] args) { String s = new String("hello");// 1 WeakReference<String> wrf = new WeakReference<String>(s); s = null;// 2 System.out.println(wrf.get());// 3 System.gc();// 4 System.runFinalization(); System.out.println(wrf.get());// 5 } }
如上,我们逐行分析。在注释 1 处,我们不能自以为聪明的用:
String s = "hello"// 6
来替代 1 中的语句,因为按照 6 中的做法,系统会采用常量池来管理这个字符串直接量,会采用强引用来引用它。同时在 1 处我们用 s 引用变量引用 “hello” 字符串对象,接下来创建一个弱引用对象来引用 s 引用的同一个对象。执行到 2 处时候,切断了 s 和引用对象的之间的联系。而我们不切断的话,会发生什么呢?自然 gc 机制无法发挥出实质性的作用,导致并没回收任何引用对象,故输出的还是字符串对象。
执行到 3 处时候,由于系统内存还足够,不会回收 wrf 对象,因此会输出 “hello” 对象。而后通知程序进行强制垃圾回收,自然弱引用对象 wrf 就会被系统回收了,此时输出的就为 null 了。
下面再看一个例子,弱引用在 Android 中的应用,我们希望在 Activity 中新建一个线程获取数据:
public class MainActivity extends Activity { //... private int page; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { //... page++; } else { //... } }; }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... new Thread(new Runnable() { @Override public void run() { //.. Message msg = Message.obtain(); msg.what = 1; //msg.obj = xx; handler.sendMessage(msg); } }).start(); //... } }
Activity 具有自身的生命周期,Activity 中新开启的线程运行过程中,可能此时用户按下了Back键,或系统内存不足等希望回收此 Activity, 由于 Activity 中新起的线程并不会遵循 Activity 本身的什么周期,也就是说,当 Activity 执行了onDestroy,由于线程以及 Handler 的 HandleMessage 的存在, 使得系统本希望进行此Activity 内存回收不能实现,因为非静态内部类中隐性的持有对外部类的引用,导致可能存在的内存泄露问题。
因此,在 Activity 中使用 Handler 时,一方面需要将其定义为静态内部类形式,这样可以使其与外部类(Activity)解耦,不再持有外部类的引用, 同时由于 Handler 中的 handlerMessage 一般都会多少需要访问或修改 Activity 的属性,此时,需要在 Handler 内部定义指向此 Activity 的 WeakReference, 使其不会影响到 Activity 的内存回收同时,可以在正常情况下访问到 Activity 的属性。
google 官方推荐的一个示例写法如下:
public class MainActivity extends Activity { //... private int page; private MyHandler mMyHandler = new MyHandler(this); private static class MyHandler extends Handler { private WeakReference<MainActivity> wrActivity; public MyHandler(MainActivity activity) { this.wrActivity = new WeakReference<MainActivity>(activity); } @Override public void handleMessage(Message msg) { if (wrActivity.get() == null) { return; } MainActivity mActivity = wrActivity.get(); if (msg.what == 1) { //... mActivity.page++; } else { //... } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... new Thread(new Runnable() { @Override public void run() { //.. Message msg = Message.obtain(); msg.what = 1; //msg.obj = xx; mMyHandler.sendMessage(msg); } }).start(); //... } }
-
虚引用:是一种最弱的引用关系,其存在的必要就是在该虚引用对象被垃圾回收器回收时候,系统能够接收到一个通知。
生存还是死亡
当一个对象被创建之后,根据它是否被引用变量所引用的状态,可以将其所处状态分为如下三种:
- 可达状态:当一个变量被创建之后,有一个或者以上的变量引用它,其就处于可达状态。
- 可恢复状态:没有任何对象来引用它,就处于可恢复状态。这种状态下,系统的垃圾回收机制准备来回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复对象的 finalize() 方法来进行资源的清理,如果系统在清理资源的同时,重新让一个引用变量引用该对象,则该对象会再次变为可达对象,反之则该对象进入不可达状态。
- 不可达状态:没用引用变量指向该对象,并且不能被恢复,则成为不可达状态。只有一个对象在不可达状态时候,垃圾回收器才会回收该对象。
然而即使某个对象被标记为不可达的情况下,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize() 方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize() 方法最多只会被系统自动调用一次),稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果要 在 finalize() 方法中成功拯救自己,只要在 finalize() 方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。接下来,我们顺便看一下 finalize() 方法:
在垃圾回收机制回收某个对象之前,通常会调用 finalize() 方法来回收一些资源。在没有明确指定回收方法之前,调用默认的 finalize() 放法,只有在调用了该方法之后,垃圾回收机制才真正的开始执行。但是我们始终要注意该方法不一定会得到执行,故我们不要指望在该方法中去执行垃圾清理的工作。finalize() 方法具有以下特点:
- 不要主动调用某个对象的 finalize() 方法
- finalize() 方法是否被调用具有不确定性
- JVM 执行可恢复对象的 finalize() 方法时候,可能会是该方法重新变为可达状态
- JVM 执行 finalzie() 方法出现异常时,不会抛出异常
接下来,我们看一个对象自我拯救的示例:
public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive(){ System.out.println("yes i am still live"); } // 留个心眼,这个方法每个对象只会被系统自动调用一次 @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method called"); 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"); } } }
输出结果:
finalize method called // 标注 finalize 被调用 yes i am still live // 逃脱成功 no,i am dead // 逃脱失败
为什么会出现对象的复活?又为什么会出现同样的代码,而对象只会复活一次?首先解决第一个问题,对象复活的奥秘完全在注释一处,我们在 finalize() 方法中执行了这样一句:
FinalizeEscapeGC.SAVE_HOOK = this;// 注释一
上面就将一个可恢复的对象变成了可达对象。而后面只能复活一次的原因是因为一个对象的 finalize() 方法只能被调用一次。
如何去回收垃圾
-
标记-清除算法
首先标记所有需要被清除的对象,然后进行回收,至于判定哪些对象该被回收,前面已经说过了。由于该算法是比较基础的算法,所以不可避免的带来两个问题:标记和清除效率不高的问题、空间碎片化的问题。
-
复制算法
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲 可用内存按容量 分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。其缺点很明显,将可用内存缩小到了一半。复制算法有如下优点:
- 每次只对一块内存进行回收,运行高效。
- 只需移动栈顶指针,按顺序分配内存即可,实现简单。
- 内存回收时不用考虑内存碎片的出现。
-
标记-整理算法
其标记过程仍然跟标记-清除算法一样。但是该算法不会直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理端边界以外的内存。
-
分代收集算法
当前用的比较多的垃圾收集算法都是分代收集。根据对象存活周期的不同,将内存分为几块。一般是将 Java 堆分为新生代和老生代,根据各个年代的特点采用合适的垃圾回收算法。在新生代中每次都有大批对象死去,只有少量存活,故而采用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对其进行分配担保,就必须使用标记-整理或者标记-清理算法来进行回收。
内存分配策略
-
对象优先在 Eden 分配
大多数情况下,对象优先在新生代 Eden 区中分配,当该区域没有足够空间时候,将发生一次 MinorGC。其中我们在复制算法中提到了:新生代 98% 对象是朝生夕死,所以没必要 1:1 分布内存空间,从而产生了一块较大的 Edge 和两块 Survivor 空间。
-
大对象直接进入老年代
-
长期存活的对象将进入老年代
虚拟机采用了分代会回收的思想来管理内存,故判断新老对象的标准就很重要。虚拟机给每个对象定义了一个对象年龄,如果对象在 Eden 区 出身,并且经过第一个 Minor GC 之后仍然存活,并且能被 Survivor 区域容纳的话,将被移动到 Survivor 区域中,并将对象年龄设置为1.对象在 S 区域中每熬过一次 MInor GC,年龄就增加1.当年龄增加到一定 15岁时候,就会被存入到老年代。年龄阈值可以通过设置指定参数来确定。
-
动态对象年龄判定
-
空间分配担保
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Go 语言的垃圾回收演化历程:垃圾回收和运行时问题
- 垃圾回收2:垃圾收集算法
- 垃圾收集3: 垃圾回收器
- 垃圾回收算法(7)-分代回收算法
- JAVA 垃圾回收机制(二) --- GC回收具体实现
- 对象回收判定与垃圾回收算法-JVM学习笔记(1)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。