文章目录
一、标记垃圾
1.1 引用计数法
每个对象记录一个被引用值,被引用一次加一次,引用取消减一次。进行垃圾回收时,如果被引用值是0,就说明是垃圾。
问题:循环引用
当 A 引用 B,B 引用 A 时,他们都不会被回收。
1.2 可达性分析
从一系列根节点进行遍历,找到能到达的每个点。不在这些点内的对象就是垃圾。
解决了循环引用问题。
可达性分析的根节点:
- 虚拟机栈中引用的对象
- 方法区中的静态对象
- 方法区中的常量对象
- 本地方法栈中的对象
二、回收垃圾
2.1 标记清除
过程:
- 标记:即第一节所述的标记方法
- 清除:每个对象依次清除。
优点:简单
缺点:
- 效率:两个过程的效率都不高
- 碎片:清除之后会产生很多不连续的碎片
回收前:
回收后:
空白的地方就是清除后的可用空间,散布得很碎。
2.2 复制(标记-复制-清除)
过程:
- 标记:即第一节所述的标记方法
- 复制:将内存平分成两部分,每次只使用其中的一块,当一块用完了,把它里面还要保留的对象复制到另一块上,然后清除这一整块。下次那块满了就重复同样的过程。
- 清除:清除掉一整块空间
优点:解决了效率问题、碎片问题
缺点:空间的利用率不高
复制过程图解:
复制前:
复制后:
复制、清除完成后,原有的 to 区就变成 from 区,开始接收对象。
复制算法的优化:
当每次要保留的对象很少时,由于复制算法将空间平分为两半,会有很大一部分空间浪费掉。
这时将内存划分为三块后可以解决这个问题,使用时,A 区 + 公共区
可以当做原有的 from 区
,垃圾回收时,把要保留的对象放在B 区
,然后B 区 + 公共区
就可以当做下一轮的from 区
,开始接收对象。
2.3 标记整理(标记-整理-清除)
过程:
- 标记:即第一节所述的标记方法
- 整理:将要保留的对象全部移动到靠前的一端,这样就把要清除的对象都放在连续的一段空间里
- 清除:清除掉连续的一段空间
优点:解决了碎片问题
缺点:效率不高
整理过程图解:
整理前:
整理后:
2.4 分代收集
根据对象存活周期的不同,把 java 堆分为新生代、老年代。
新生代中,对象的存活率不高,复制算法的成本低,且有老年代可以进行分配担保,所以使用复制算法。即包括一个 Eden 区
(即上述的公共区)、一个 from Survivor
区(A 区)、一个 to Survivor
区(B 区)。
老年代中,对象的存活率较高,且无人担保,所以使用标记清除或标记整理法。
分代模型:
- 堆
- 新生代(minor GC)
- Eden
- from Survivor
- to Survivor
- 年老代(major/full GC)
- 新生代(minor GC)
- 方法区
- 永久代
三、内存分配与回收策略
3.1 分配与回收流程
- 对象优先放在 Eden,Eden 放不下了就 minor GC,把存活的放入 to Survivor。然后将 to Survivor 和 from Suivivor 互换。(这时 Eden、to Survivor 已经被清空,from Survivor 中存放着上一次 gc 保留的对象。)
- 继续把对象放在 Eden 中,Eden 放不下了就 minor GC,把存活的放入 to survivor。把 from survivor 的也放入 to survivor。然后将 to survivor 和 from suivivor 互换。(这时 Eden、to Survivor 已经被清空,from Survivor 中存放着上一次 gc 保留的对象。)
注意:如果 to Survivor 中的空间不足以存放要保留的对象,那对象会被放入老年代,这样的机制称为分配担保
。
3.2 提高效率
为提高效率,避免在 Eden 和 Survivor 中发生大量的内存复制。虚拟机采取了以下措施:
- 大对象直接进入老年代
大对象不经过新生代,直接进入老年代 - 长期存活的对象将进入老年代
每个对象都有一个年龄,survivor 一次加一。当它的年龄熬到一定岁数,说明以后也很可能不会被回收,于是移到老年代中。 - 动态对象年龄判定
如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,这说明同一时期创造了很多有用的对象,他们的生命周期很可能是一致的,对他们进行复制比较耗时,所以移到老年代中。
3.3 空间分配担保
如上所述,在新手代中进行的 GC 是 minor GC
,需要老年代进行分配担保
,但当老年代也没有足够的空间去容纳要保留的对象,那就需要进行一次 full GC
,清除老年代无用的对象。