Java虚拟机(二)——垃圾回收器与分配策略

对于Java虚拟机(一)——Java内存区域与内存溢出异常中所讲的程序计数器 、虚拟机栈、本地方法栈,这3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出执行入栈和出栈操作。这3个区域的内存分配和回收都具有确定性,不需过多考虑回收的问题,因为方法结束或线程结束时内存就跟着回收了。垃圾回收关注的是方法区和Java堆
方法区:永生代
Java堆:新生代和老年代

对象已死吗

引用计数算法(reference counting)

原理:给对象添加一个引用计数器,每当有一个对象引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0就表示该对象不可能再被使用。
然而主流Java虚拟机都没有使用引用计数法来管理内存,因为很难解决对象之间的循环引用的问题。

可达性分析算法(reachability analysis)

原理:通过一系列GC Roots对象作为起点,从这些节点开始往下搜索,搜索所走过的路径叫做引用链(reference chain),当一个对象到GC Roots没有任何引用链时,则说从GC Roots到这个对象不可达,则这个对象不可用。
Java中可作为GC Roots的对象:

  • 虚拟机栈中的引用对象;
  • 方法区中类静态属性的引用;
  • 方法区中常量引用的对象
  • 本地方法(Native方法)中引用的对象。

但可达性分析法要真正宣告一个对象的死亡,需要经历两次标记。第一次是发现没有与GC Roots对象的引用链,判断此对象是否有必要执行finialize()方法。如果判断此对象有必要执行finalize()方法,则这个对象会放进一个叫F-Queue的队列中,由虚拟机自动建立的低优先级的Finalizer线程去触发finalize()方法。然后GC将对F-Queue进行第二次标记,也就只有在这个阶段,对象有唯一一次自救机会,因为每个finalize()方法最多只会被系统自动调用一次。但不建议这样拯救对象,因为代价太高。

回收方法区

在方法区(或HotSpot虚拟机中的永久代)中回收垃圾性价比比较低,而在堆中,常规应用进行一次垃圾回收 一般可以回收70%~95%的空间。
方法区(永生代)的垃圾收集主要回收两部分的内容:废弃常量和无用类。
判定一个常量是否是无用类:没有其它对象引用它,就会被系统清理出常量池。
而判定一类是否是无用类则比较复杂,需要同时满足下面3个条件:

  • 该类的所有实例都已被回收;
  • 加载该类的ClassLoader已经被回收;
  • 该类java.lang.Class对象没有在任何地方被引用,即无法通过反射访问该类的任何方法。

垃圾回收算法

标记-清除(mark-sweep)算法

标记-清除算法是最基础的回收算法。
原理:该算法被分为“标记”和“清除”两部分。首先标记处所有需要回收的对象,然后在标记完成后统一进行回收。它的标记过程就是1.2节所讲的对象标记判定。

缺点:一是效率问题,“标记”和“清除”的效率都高;二是空间问题,容易产生不连续的碎片。
之所以叫它最基础的算法,是因为后来的算法都是对这种算法的不足进行改进。

复制(copying)算法

为了解决效率问题,提出了复制算法。
原理:将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就把还活着的对象复制到另一个块上面,然后将使用过的这块一次清理掉。这样 就不用考虑内存碎片的问题。

缺点:将内存缩小了一般,空间利用率不高。
现在商业虚拟机都采用这种方法来回收Java堆新生代,新生代是“朝生夕死”,将内存空间划分为较大的Eden区和两块较小的Survivor区。每次回收时,将Eden区和一块Survivor区中的存活对象复制到另一块Survivor区中,然后将刚才用过的Eden区和Survivor区一次清理掉。

标记-整理(mark-compact)算法

复制收集算法在对象存活率较高时,就会进行较多的复制操作,效率将会变低。提出了标记-整理算法。
原理:其标记过程以标记-清除算法类似,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

分代收集(generational collection)算法

当前商业虚拟机的垃圾收集都采用分代收集算法,它是根据存活周期的不同把内存划分为几个块一般把Java堆分为(young generation)和老年代(tenured generation)。新生代每次垃圾回收都有大量对象死去,只有少数存活,因此使用复制算法老年代对象存活率高,没有额外空间对它进行分配担保,因此使用标记-清除算法或标记-整理算法

垃圾回收器

如果说收集算法是内存回收的方法论,那么垃圾回收器就是内存回收的具体实现。

Serial收集器

Serial收集器是最基本的收集器,它是一个单线程的收集器,在它进行垃圾回收的时候,必须暂停所有的其它工作线程,直到它收集结束。是虚拟机运行在Client模式下的默认新生代收集器。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本。是运行在Server模式下的首选新生代收集器。
它的目标是缩短垃圾收集时用户线程的停顿时间。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,它也使用复制算法。它与ParNew不同的是,它的目标是达到一个可控的吞吐量(throughput)。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短越适合需要与用户交互的程序,而高吞吐量则可以高效地利用CPU的时间,尽快完成程序的运行任务。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,使用单线程和标记-整理算法。主要给Client模式下的虚拟机使用。

Parallel Old收集器

Parellel Old收集器是Parellel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

CMS(concurrent mark sweep)收集器

CMS是一种以获取最短停顿时间为目标的收集器。它使用标记-清除算法。目前很大一部分应用集中在互联网站和B/S系统的服务端上,这类应用尤其注重服务器响应速度。

G1(garbage first)收集器

G1收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务器的垃圾收集器。与其他GC服务器相比,G1具备的特点:并行与并发、分代收集、空间整合、可预测的停顿。

内存分配与回收策略

对象优先在Eden分配

大多数情况下,对象在新声代Eden区中分配。

大对象直接进入老年代

大对象是指,需要大量连续内存空间的Java对象,最典型的大对象是那种很长的字符串和数组。

长期存活的对象将进入老年代

为了识别哪些对象应该放在新生代和老年代,虚拟机为每个对象定义了对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Surviver容纳的话,将被移到Surviver空间,并且年龄加1。对象在Surviver空间每熬过一次Minor GC,则年龄加1,增加到一定程度(默认为15)则进入老年代。

  • Minor GC:新生代空间(包括Eden和Survivor区域)回收内存;
  • Major GC:清理老年代;
  • Full GC:清理整个堆空间:包括新生代和老年代。

动态对象年龄判断

为了更好地适应不同程序的内存情况,并不要求对象年龄计数器必须达到15才进入老年代。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代所有可用空间是否大于所有新生代对象总空间,如果成立,则Minor GC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure值是否设置为允许担保失败。