java的垃圾收集器与内存分配策略--兼具算法理论与java实现

java的垃圾收集器与内存分配策略

垃圾收集需要完成的三件事情

  1. 哪些内存需要回收
  2. 如何回收
  3. 什么时候回收

垃圾收集针对的java内存区域

程序计数器、虚拟机栈、本地方法栈三个内存区域为线程私有,线程结束时内存会回收,内存的分配和回收在编译期能够确定下来,所以不需要垃圾收集
java堆是线程共享的,在编译期无法知道需要的内存大小,在运行期动态分配回收内存,所以垃圾收集针对的内存区域为java堆。

哪些内存需要回收

判断哪些内存需要回收是垃圾收集需要完成的第一件事。内存需要回收以为着该片内存上的对象不再存活。

引用计数算法

Reference Counting 每个对象都有一个引用计数器,每当有地方引用它时,计数器数值+1,引用失效时,计数器数值-1.数值为0时对象的内存需要回收。
引用计数算法无法避免循环引用的问题。
java使用的不是引用计数算法

可达性分析算法

在主流的商用程序语言中(java、c#,lisp)的实现中,都是通过可达性分析算法(Reachability Analysis)来判断对象是否存活。
算法思路为通过一些称为”GC Roots“的对象作为起始点,遍历搜索所有跟他们有关联的对象。把每一个对象作为一个节点,两个对象有关联时两个节点间连线,对象与对象之间形成了图的数据结构,从GC Roots出发遍历这张图,能遍历到的对象为可达对象,不能遍历到的为不可达对象。不可达的对象是不再存活,可回收的对象。
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

可达性分析算法可以解决循环引用的问题
java中,GC Roots对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native 方法)引用的对象

四种引用方式

判断对象是否存活跟引用十分有关,在jdk1.2后,引用可以分为四种方式

  1. 强引用(Strong Reference),强引用是程序中最常用的,如String str=“12”; Object obj=new Object(); 只要强引用还存在,垃圾收集器就不会回收该对象。内存不够时会抛出outofmemory也不会回收存在强引用的对象
  2. 软引用(Soft Reference,在内存不足,将要抛出内存溢出异常前,会把只跟软引用关联的对象列进回收范围,如果回收后还是内存不足,才抛出内存溢出异常。
  3. 弱引用(Weak Reference)jvm进行垃圾回收时,无论内存是否足够,都会回收只跟弱引用关联的对象。
  4. 虚引用(Phantom Reference)一个对象是否存在虚引用,对它的存活时间不造成影响,无法通过虚引用获取对应的对象。为对象设置虚引用的目的在于该对象被垃圾回收时通过引用队列收到一个系统通知

四种引用的详细解释与例子参考https://blog.****.net/linzhiqiang0316/article/details/88591907

如何回收

这一节讨论不需要的内存如何回收,涉及内存回收算法,java中的分代收集算法和不同的垃圾收集器

内存回收算法

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

分为标记和清除两个阶段,标记阶段就是用可达性分析算法标记出所有不可达对象,清除阶段则是把所有标记的内存清除掉。
java的垃圾收集器与内存分配策略--兼具算法理论与java实现
标记-清除算法效率不高,且清除结束后会留下大量不连续的内存碎片,导致后续大对象分配内存时无法找到足够连续的内存而不得不出发垃圾收集动作

复制算法

把内存划分为容量相同的两块,每次使用其中一块,使用的一块内存用完了,把还存活的对象复制到另一块空闲内存,把之前使用的一块内存直接清理掉。
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

复制算法的优点是不存在内存碎片问题,内存回收过程简单高效,代价是每次只能会用一半内存,当存活对象较多时,把存活对象复制到空闲内存块的代价较大。

标记-整理算法

标记过程与标记-清楚算法一致,但后续不是直接清理可回收对象的内存,而是把存活对象往一个方向移动,把存活对象压在一个密集的区域,然后清理区域外的连续内存
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

分代收集算法

根据对象的存活周期把内存划分为几块,即把对象分为几代,然后可以根据每一代的特点采用不同的垃圾收集算法,这就是分代收集算法。

java的内存回收算法

java堆中按照分代收集算法一般分为新生代和老年代。新生代又可以分为Eden、From、To 三个区域 From和To统称Survivor
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

  • 新生代中每次垃圾收集都会有大量对象死去,只有少数对象存活,所以适合复制算法,复制算法内存按1:1划分,是考虑到所有对象都存活的极端情况,此时复制的目的内存区域必须大于等于对象所在的内存区域。但研究表明新生代中98%的对象是“朝生夕死”的,所以上述所有对象存活的情况基本不会出现,基于此,HotSpot虚拟机把新生代的内存按8:1:1的比例划分为Eden和两个Survivor
    每次使用Eden和一个Survivor,使用的Survivor为From,未使用的为To,回收时,把Eden和From中的存活对象赋值到To中,然后清除Eden和From中的内存,随后To从角色上变为了From,From从角色上变为了To,系统使用新的Eden和From分配内存。
    基于此,HotSpot虚拟机中每次新生代可用的内存空间为整个新生代内存空间的90%,考虑特殊情况,内存回收时存活对象多于整个新生代内存空间的10%,无法完全复制到To中,此时需要依赖其他内存(指老年代)进行分配担保(Handle Promotion)
  • 老年代中对象存活率高,且没有额外空间进行分配担保,所以必须使用标记-清除算法或者标记-整理算法

对象何时进入老年代

  1. 大对象直接进入老年代
    因为新生代是使用的复制算法,所以要尽量减少复制的内存,所以对象内存到一定的值后就会直接进入老年代。通过参数-XX:PretenureSizeThreshold 可以设置对象直接进入老年代的大小阈值,超过设置值大小的对象直接进入老年代
    PretenureSizeThreshold只对Serial和ParNew收集器有效
  2. 长期存活的对象进入老年代
    每个对象会有一个Age的计数器,初始值为0,在新生代中每经过一次minor GC并且存活(进入To区域一次),这个对象的Age就会加1,如果增加到一定程度(默认为15)。那么就会进入老年代中。
    通过参数-XX:MaxTenuringThreshold可以设置进入老年代的age阈值
  3. 动态对象年龄判定
    如果在新生代存活区中相同年龄所有对象大小的总和大于存活区的一半,年龄大于或等于该年龄的对象就会直接进入老年代。
    比如现在存活区有三个对象,Age分别为2、2、3。那么Age为3的这个对象就会进入老年代。

不同的垃圾收集器

此处讨论的额垃圾收集器基于jdk1.7 update14之后的HotSpot虚拟机

java的垃圾收集器与内存分配策略--兼具算法理论与java实现

两个收集器间存在连线说明可以搭配使用

Serial收集器

最简单,历史最悠久的收集器,单线程运行,在进行垃圾收集时,必须停掉所有的工作线程,即“Stop The World”
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

与其它单线程收集器相比简单而高效,适合用在单个CPU的环境
是虚拟机在client模式下的默认新生代收集器

ParNew收集器

多线程版本的Serial收集器
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

是虚拟机在Server模式下的首选新生代收集器,因为除了Serial收集器,只有它能和CMS收集器(一款跨时代的并发收集器)配合工作。

ParNew收集器在单CPU下不会比Serial收集器效果好,在多CPU的条件下可以有效利用系统资源

Parallel scavenge收集器

多线程版本的新生代收集器,使用复制算法
Parallel scavenge收集器关注的是可控的吞吐量,而其他收集器关注缩短垃圾收集时用户线程的停顿时间
java的垃圾收集器与内存分配策略--兼具算法理论与java实现

Parallel scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy 打开以后虚拟机会根据运行情况动态调整参数提供最合适的停顿时间或者最大吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

Serial Old收集器

Serial收集器的老年代版本。单线程,采用标记-整理算法
主要用于client模式下的虚拟机

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
跟Parllel Scavenge收集器搭配使用称为“吞吐量优先”的收集器

CMS收集器

Concurrent Mark Sweep收集器以获取最短回收停顿时间为目标
CMS收集器有四个阶段

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

初始标记和重新标记需要“Stop The World”
初始标记标记GC Roots能直接关联到的对象
并发标记是进行GC Roots Tracing的过程
重新标记是为了修正部分对象的标记,这些对象在并发标记阶段因用户程序继续进行导致其标记发生变动

耗时最长的并发标记与并发清除可以与用户程序并发进行

java的垃圾收集器与内存分配策略--兼具算法理论与java实现

G1收集器

略过

什么时候回收

这一节讨论何时触发GC。
GC分为minor GC和Major GC(Full GC)

  • minor GC 是发生在新生代的垃圾收集动作,发生较频繁,触发 minorGC的条件如下–新对象需要进入新生代内存区而Eden区的内存不足以存放新对象时触发minor GC
  • Major GC /Full GC 是发生在老年代的垃圾收集动作,经常但不一定会触发至少一次的Minor GC,一般GC速度比Minor GC慢十倍。当对象从新生代或者直接进入老年代,但老年代所剩内存不足提供给对象,则会出发Major GC

程序中可以显示调用System.gc 但调用时不一定会触发GC,而能会在后面某个时间再进行GC,GC的触发对于程序员来说其实是透明的,无法准确预知GC发生的时间

总结

本文讨论了垃圾收集需要考虑的三件事情–哪些内存需要回收、如何回收和什么时候回收,基于此讨论垃圾收集的经典算法–可达性分析算法、标记-清除算法、复制算法、标记-整理算法和分代收集算法。其中在理论的基础上对java中具体的的内存回收算法以及多个版本的垃圾收集器进行了介绍。