判断对象是否需要回收
- 引用计数算法
循环引用场景会引起内存泄漏。
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name){}
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
如果先释放a/b.instance引用,再释放a/b引用,就不会出现内存泄露的情况。
这里先释放了a/b引用,就找不到a/b对象,也就没办法再释放a/b.instance引用。
- 可达性分析法
可达性分析算法(Reachability Analysis)的基本思路是,通过根引用(GC Roots)作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
Gc Roots:
- 虚拟机栈****中的对象引用。<堆中的对象引用并没有作为GC Root>
- 方法区中静态的****对象引用。
- 方法区中常量的对象引用。
- 本地方法栈中JNI(即一般说的 Native 方法)的对象引用。
回收算法
- 标记清除算法<容易带来内存碎片>
- 复制算法<空间利用率低>
内存空间划分为1区,2区。jvm标记可达对象,并复制到2区,1区清零。
- 标记整理算法<效率低>
标记整理算法(Mark-Compact)标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。标记整理算法一方面在标记 - 清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。
-
分代回收算法
-
jvm堆结构
- 各区域回收算法
分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述 3 种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。jvm根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记 - 清理或者标记 — 整理算法来进行回收。
-
young区
-
区域划分原理
为什么需要survivor
不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
为什么需要两个(包括from区和to区)
设置两个 Survivor 区最大的好处就是解决Survivor 区内存碎片化。
我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有 2 个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。
-
old区:
-
回收机制
老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法。
参考:
GC执行
- GC类型
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
-
Partial GC:并不收集整个GC堆的模式
-
Young GC:只收集young gen的GC。
-
Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式。
-
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式。
-
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
蚂蚁内部模式:yong gc+full gc,不存在Old GC和Mixed GC
- 对象进入old区条件
当对象首次创建时, 会放在新生代的eden区, 若没有GC的介入,会一直在eden区, GC后,是可能进入survivor区或者年老代:
-
**Survivor区满。**yong gc后survivor区剩余空间不足,无法承接eden区的存活对象。
-
长期存活对象。当对象年龄达到一定的大小就会离开年轻代,进入老年代。 而对象的年龄是由GC的次数决定的-XX:MaxTenuringThreshold=n 新生代的对象最多经历n次GC,就能晋升到老年代。<默认为16跳>
-
大对象**,减少from to区的复制成本****。除年龄外, 对象体积也会影响对象的晋升的, 若对象体积太大, 复制成本很高。**-XX:PretenureSizeThreshold 即对象的大小大于此值, 就会绕过新生代, 直接在老年代分配。
-
Full gc触发条件
-
当准备要触发一次 young GC时,如果发现统计数据说之前 young GC的平均晋升大小比目前的old gen剩余的空间大,则不会触发young GC而是转为触发 full GC。
-
大对象直接到old区申请空间,发现old区内存不够。