3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略

3.1概述

哪些内存需要回收?[插图]什么时候回收?[插图]如何回收?

3.2对象已死了吗?

在堆中存放在几乎所有的java对象的实例,垃圾回收器在回收前,第一件事就是确定哪些对象是活的哪些对象已经死了

3.2.1引用计数算法

常用的是引用及计数算法:即给对象添加一个计数器,每当有地方引用它,计数器就加1,当引用失效,计数器就减一,任何时候计数器为0 的对象就是不可再用的对象,就是垃圾回收的目标.但是主流的java虚拟机没有使用引用计数算,因为很难解决对象之间的相互循环引用的问题.

举个简单的例子,testGC()方法:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略

从运行结果中可以清楚看到,GC日志中包含“4603K->210K”,意味着虚拟机并没有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。

3.2.2可达性分析算法

在主流的语言,java,C++称都是使用可达性分析算法.判断对象是否存活.其基本思路的是,从一个叫GCRoots对象作为起点,向下搜索,当一个对象与gcroot之间没有引用链时,这个对象就是被回收的目标,即使这个对象与其他对象有关联,仍然是被回收的目标.

 

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略\

3.2.3再谈引用

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

3.2.4生存还是死亡

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

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。从代码清单3-2中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。

从finalize()方法中我们可以看出,对象可以自救,并且只有一次自救机会

3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略

3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略

3.2.5回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类.在方法区里使用垃圾回收效率很差,性价比很低.回收同堆里的对象回收,就是看有无String对象引用这个常量,如果没有,就会发生回收

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:(判断方法区里的类是否是无用的类,需要满足三个条件,①该类的实例在堆中已经被回收了,②该类的类加载器已经被回收③关于该类的class对象没有在任何地方呗引用,也没任何地方使用反射访问该类的方法)

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持。

3.3垃圾收集算法

3.3.1标记清除算法

最基础的算法是标记清除算法.(mark-sweep) ,算法分为标记和清除两个阶段:先标记要回收的对象在标记完成后统一进行回收被标记的对象.这种方法有两种不足:①效率问题,②空间问题标记清除之后会产生大量不连续的内存碎片3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略

 

3.2.2复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。复制算法的执行过程如图3-3所示。3.深入理解java虚拟机--第二部分--- 垃圾收集器与内存分配策略

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor[插图]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(HandlePromotion)。

3.3.3标记-整理算法

复制算法在对象存活率较高时进行较多的复制操作,效率将会变低.所以老年代一般不能直接选用这种算法

标记-整理算法(mark-compact),标记过程仍然与标记-清除一样,但后续步骤不是直接对可回收对象进行清理,而是让存活的对象想一端移动