安卓面试Android 源码分析

《深入理解Java虚拟机》笔记之JAVA内存模式与垃圾回收

2017-02-07  本文已影响137人  Hohohong

文章作为《深入理解Java虚拟机》读书笔记,讲的可能就没书本详细。

Java内存模型

Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示:

运行时数据区域

局部变量表:存放了编译期可知的各种基本数据类型,对象引用。其中64位长度的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用一个。如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常。如果动态扩展时无法申请到足够的内存,则抛出OutOfMemoryError异常。

虚拟机对象

内存回收GC

前面内存模型讲到5个数据区域,其中程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。在这几个区域就不需要过多考虑内存回收问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,这部分内存的分配和回收都是动态的。

垃圾对象如何确定?
Java堆中存放着几所所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定哪些对象还"活着",哪些已经"死亡",也就是不会被任何途径使用的对象。 对象存活判定方法:

引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。其原理是:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,主流Java虚拟机没有选用引用计数法来管理内存。

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

可达性分析算法

在Java语言中,可作为GC Roots的对象包括下面几种:
①虚拟机栈(栈帧中的本地变量表)中引用的对象。
②方法区中类静态属性引用的对象。
③方法区中常量引用的对象。
④本地方法栈中JNI(即一般说的Native方法)引用的对象。

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

程序中可以通过覆盖finalize()来一场"惊心动魄"的自我拯救过程,但是,这只有一次机会。

/**  
 * 此代码演示了两点:  
 * 1.对象可以在被GC时自我拯救。  
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次  
 * @author zzm  
 */  
public class FinalizeEscapeGC {  
 
  public static FinalizeEscapeGC SAVE_HOOK = null;  
 
  public void isAlive() {  
   System.out.println("yes, i am still alive :)");  
  }  
 
  @Override  
  protected void finalize() throws Throwable {  
   super.finalize();  
   System.out.println("finalize mehtod executed!");  
   FinalizeEscapeGC.SAVE_HOOK = this;  
  }  
 
  public static void main(String[] args) throws Throwable {  
   SAVE_HOOK = new FinalizeEscapeGC();  
 
   //对象第一次成功拯救自己  
   SAVE_HOOK = null;  
   System.gc();  
   //因为finalize方法优先级很低,所以暂停0.5秒以等待它  
   Thread.sleep(500);  
   if (SAVE_HOOK != null) {  
    SAVE_HOOK.isAlive();  
   } else {  
    System.out.println("no, i am dead :(");  
   }  
 
   //下面这段代码与上面的完全相同,但是这次自救却失败了  
   SAVE_HOOK = null;  
   System.gc();  
   //因为finalize方法优先级很低,所以暂停0.5秒以等待它  
   Thread.sleep(500);  
   if (SAVE_HOOK != null) {  
    SAVE_HOOK.isAlive();  
   } else {  
    System.out.println("no, i am dead :(");  
   }  
  }  
} 



运行结果为:
finalize mehtod executed!  
yes, i am still alive :)  
no, i am dead :(

任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。因此第二段代码的自救行动失败了。

前面的算法讲的是如何判定垃圾对象,判定完后则该如何处理进行垃圾回收?

典型的垃圾回收算法

1.Mark-Sweep(标记-清除)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

标记-清除

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2.Copying(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

复制

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

3.Mark-Compact(标记-整理)算法

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

标记-整理

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(一般为8:1:1),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact标记-整理算法。


参考资料:《深入理解Java虚拟机》2章和3章内容

上一篇 下一篇

猜你喜欢

热点阅读