引言

垃圾收集技术并不是Java语言首创的,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。垃圾收集技术需要考虑的三个问题是:

哪些内存需要回收?
什么时候回收?
如何回收?

http://segmentfault.com/a/1190000002931555 中讲到java内存运行时区域的分布,其中程序计数器,虚拟机栈,本地方法区都是随着线程而生,随线程而灭,所以这几个区域就不需要过多考虑回收问题。但是堆和方法区就不一样了,只有在程序运行期间我们才知道会创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的就是这部分内存。

一 对象死亡判据

垃圾收集器在对一个对象回收之前,首先要判断对象在程序中是否还有使用的可能性,充要条件就是没有被程序可访问的引用再指向这个对象实例。最简单的办法就是给对象实例添加中添加一个引用计数器,每当有一个引用指向它时,计数器就加一,当引用失效时,计数器就减一,如果计数器值为0则说明没有引用指向它,可以进行回收。但是这个方法中计数器为0并不是一个必要条件,例如,生成两个对象实例,每个对象实例的属性都指向对方,那么这个两个对象实例分别最少有一个引用。

java采用的是可达性分析算法,即找一部分对象作为"GC Roots"节点,从这些节点开始向下搜索,当某个对象到"GC Roots"节点没有可达路径时,说明此对象是不可用的。在java中作为"GC Roots"的节点包括:

  1. 虚拟机栈中引用的对象,

  1. 方法区静态属性引用的对象,

  2. 方法区常量引用的对象,

  3. 本地方法区中本地调用所引用的对象。

引用扩充

如果reference类型的数据中存储的数值是另一块内存的起始地址,那么这块内存就代表着一个引用。一个对象在这种状态下,只能有被引用和没有被引用两种状态。java对引用概念进行了扩充,将引用分为强引用(new),软引用(softReference),弱引用(WeakReference),虚引用(PhantomReference)。如果强引用存在,则垃圾收集器不会回收该对象。如果系统即将发生内存溢出异常,那么垃圾回收集器则会回收软引用对象。弱引用对象只能存活到下一次垃圾收集之前。虚引用对象不会对其生存时间构成任何影响。

对象的自我救赎

在垃圾收集器发现某一个对象到"GC Roots"路径不可达时,先会判断该对象是否覆盖finalize()方法,或是否执行过finalize()方法。如果覆盖了且没有执行过该方法,则会将该对象放到低优先级的Finalizer线程队列中去执行finalize()方法,如果在finalize()方法中该对象又被引用,则会有一次逃脱被回收的命运。

方法区的回收

方法区中主要回收废弃的常量和无用的类。对于常量,如果没有引用指向常量,则该常量会被回收。对于类的回收则麻烦许多,首先要判断该类是无用的类无用的类要满足三个条件:

  1. 所有类的实例被回收。

  2. 加载该类的ClassLoader已经被回收。

  3. Class没有被引用,不会通过反射访问该类的方法。

二 垃圾回收算法

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

该算法分为两个阶段:首先标记处要回收的对象,标记完成后统一回收所有被标记的对象。
存在的问题:

  1. 标记和清除效率都不高

  2. 标记清除后会产生大量内存碎片,分配大对象时可能触发另一次垃圾收集。

复制算法(Copying)

该算法将内存分为两个等大小的区域,每次只使用一个区域。当一个区域快用完了,就将这个区域中存活的对象复制到另一个区域。

优点是避免了内存碎片的产生,缺点是浪费内存空间。

有公司研究表明,新生代的对象98%都是朝生暮死,所以虚拟机把新生代内存划分为一个较大的Eden空间和两个较小的Survivor空间。每次只是用Eden空间和一个Survior空间,当进行复制清理时,将Survivor空间和Eden空间中存活的对象复制到另一块Survivor空间。当Survivor空间不够用时,就会依赖老年代进行分配担保。

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

针对老年代对象存活率高的情况,复制算法明显不合适,于是采用标记整理算法,标记和标记清除算法相同,二后边的整理则是让所有存活的对象都向一端移动,然后清理掉边界外的内存。

分代收集

当前虚拟机都采用分代收集,分代的依据是对象的存活周期。一般新生代存活率低,采用复制算法。老年代存活率高采用标记整理标记清除

一般来讲,新生代空间会小很多,具体比例一般要看应用场景。而默认的大小一般是老年代的1/4到1/3。

三 垃圾收集器


由于虚拟机采用了分代收集,所以针对不同代收集器也不同。上图是HotSpot虚拟机的垃圾收集器,连线表示可以协同工作。

Serial收集器,复制算法,它是一个单线程的收集器,并且在进行收集时会暂停其他线程,它默认是client模式下的新生代收集器。

ParNew收集器是Serial收集器的多线程版,它是第一款并发收集器。

Parallel Scavenge收集器可以精确控制吞吐量(用户代码运行时间/(用户代码时间+垃圾收集时间))

SerialOld收集器是serial收集器的老年版,采用标记整理算法,同样是单线程收集器。

ParallelOld是ParallelScavenge收集器的老年版,使用多线程和标记整理算法。

CMS收集器是以最短回收停顿时间为目标的收集器,采用标记清除算法,在重视响应速度的系统中得以应用。但是缺点是对CPU资源敏感,无法处理浮动垃圾,易产生内存碎片。

G1收集器是最新推出的收集器,可应用在JDK1.7u4及以上版本。它将内存分为多个Region,新生代和老年代分别包含多个Region。G1跟踪各个Region,判断垃圾价值大小,优先回收价值最大的Region。

安全点

安全点的概念是指当进行GC时,应当让工作线程停止,这时会更容易对对象是否存活进行判断。而停止线程应当在安全的时刻,所以会有安全点的概念。

暂停线程有两种方式,第一种是强制暂停,如果某些线程没有到达安全点则再让他运行到安全点,这叫做抢先式中断。第二种是在编译过程中在安全点加入一个条件判断判断0x160100内存页是否可读,如果不可读则会暂停,这叫做主动式中断

四 内存分配与回收策略


对象的分配,就是在堆上分配,对象主要分配在新生代的Eden区域中,如果启动了本地线程分配缓冲,则按线程优先在TLAB中分配。少数情况也有可能直接分配到老年代。

对象在Eden区域分配时,当Eden区域没有足够空间,虚拟机会发起一次新生代垃圾收集。

如果对象需要大量连续内存空间,例如String类型和数组。大对象对于虚拟机内存分配来说是一个坏消息,朝生暮死的大对象是要命的坏消息。经常出现大对象会导致多次出发垃圾收集。对于这类对象,可以设置参数将大对象直接存入老年代。

每一个对象都有一个年龄计数器,当对象在Eden区域出生,每经过一次GC,并且存入Survivor,计数器加一。当年龄增加到一定程度(默认15),则会被存入老年代。同时,如果Survivor空间中相同年龄对象占空间超过50%,则也会直接进入老年代。

总结

垃圾收集算法:复制算法标记-清除算法标记-清理算法

垃圾收集器特点:新生代用复制,老年代用标记清理,CMS用标记清除。

Eden空间大小和Survivor空间大小默认比率为8:1,即新生代10%的空间用来存放复制后的对象。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐