-
垃圾回收:释放垃圾占用的空间,防止内存泄露,有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
-
垃圾回收的原因:Java语言一个显著的特点就是引入垃圾回收机制,使C++程序员最头疼的内存管理的问题迎刃而解,因为有了垃圾回收机制,Java的对象不在有"作用域的概念",只有对象的引用才有作用域,垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
-
垃圾回收主要在堆里面发生
-
当堆中内存的使用率超过一个值时就会触发垃圾回收
-
判断对象是否存活–引用计数法
通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count),堆中每个对象都有一个引用计数。当一个对象被创建的时候,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其他变量被赋值为这个对象时,计数加1,但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当做垃圾回收。当一个对象实例被垃圾回收时,它引用的任何对象实例的引用计数器减1.
-
引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。
-
引用计数法的优缺点
- 效率较高
- 堆里面的对象实例循环引用时,它们的引用计数永远不可能为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。(Java8采用的不是这种方式)
-
判断对象是否存活–可达性分析法(又叫根搜索法)
跟搜索算法是从离散数学中图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,及无用的节点。
-
通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。
-
Java中可以作为GC ROOT的对象有:
- 虚拟机栈中引用的对象(局部变量表)
- 方法区中静态属性引用的对象(本地方法栈)
- 方法区中常量引用的对象
- 本地方法栈引用的对象
-
垃圾收集算法–标记清除算法,标记清除算法(Mark-Sweep)是最基础的一种垃圾回收算法,它分为2部分:
- 首先标记出所有需要回收的对象
- 在标记完成之后统一回收所有被标记的对象(GC ROOT)
-
标记清除算法的缺点
- 效率问题,标记和清除两个过程的效率都不高;
- 空间的问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多有可能会导致
-
垃圾收集算法–复制算法,复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。主要用于回收新生代的对象,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。
-
垃圾收集算法–标记整理算法:标记过程与标记清除算法一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
-
标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。
-
垃圾收集算法–分代收集算法,分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 — 整理算法来进行回收。
-
内存模型与回收策略:
-
Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域。
-
Java 堆主要分为2个区域:年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。如下图:
-
Eden 区
大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
-
Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。
Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
设置两个 Survivor 区最大的好处就是解决内存碎片化。
如果Survivor只有一个区域。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。
-
Old 区
老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法。
-
在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代
-
大对象
大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。
-
长期存活对象
虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加1岁。当年龄增加到15岁时,这时候就会被转移到老年代。当然,这里的15,JVM 也支持进行特殊设置。
-
动态对象年龄
虚拟机并不重视要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。
-
深入浅出JVM之垃圾回收
最新推荐文章于 2023-07-11 09:49:32 发布