Java JVM 垃圾回收

参考链接https://www.cnblogs.com/1024Community/p/honery.html#%E4%B8%80----%E6%8A%80%E6%9C%AF%E8%83%8C%E6%99%AF%E4%BD%A0%E8%A6%81%E4%BA%86%E8%A7%A3%E5%90%A7

JVM 的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而 Java 堆区和方法区这部分内存的分配和回收是动态的。

判断对象是否存活的算法

无论引用计数算法还是可达性分析算法都是基于强引用而言的。

引用计数算法

算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

优缺点

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为 0。

循环引用代码
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;

最后虽然将 object1object2 赋值为 null,也就是说 object1object2 指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

可达性分析算法

程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

img

GC Roots 对象

在 Java 语言中,可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈中引用的对象(栈帧中的局部变量表);

  • 方法区中类静态属性引用的对象;

  • 方法区中常量引用的对象;

  • 本地方法栈中 JNI(Native方法)引用的对象。

JNI,Java Native Interface,Java 本地接口

对象死亡(被回收)前的最后一次挣扎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;

第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将会被真的回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

方法区如何判断是否需要回收

方法区存储内容是否需要回收的判断有所不同。方法区主要回收的内容有:废弃常量无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
  • 加载该类的 ClassLoader已经被回收;
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记-清除算法

原理:分为标记和清除两个阶段:首先标记出所有存活的对象,在标记完成以后统一回收未被标记的对象。

特点:(1)效率问题,标记和清除的效率都不高;(2)空间的问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会导致程序运行过程需要分配较大的对象时候,无法找到足够连续内存而不得不提前触发一次垃圾收集。

应用 :适合在老年代进行垃圾回收,比如 CMS 收集器就是采用该算法进行回收的。

缺点

  • 效率问题:标记和清除两个过程的效率都不高;
  • 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-整理算法

原理:分为标记和整理两个阶段:首先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

特点不会产生内存碎片,但是整理会花一定的时间。

应用:适合老年代进行垃圾收集,parallel Old(针对parallel scanvange gc 的) gc 和 Serial old 收集器就是采用该算法进行回收的。

复制算法

原理:它先将可用的内存按容量划分为大小相同的两块,每次只是用其中的一块。当这块内存用完了,就将还存活着的对象复制到另一块上面,然后把已经使用过的内存空间一次清理掉。

特点没有内存碎片,只要移动堆顶指针,按顺序分配内存即可。代价是将内存缩小为原来的一半。

应用:适合新生代进行垃圾回收。serial new,parallel new 和 parallel scanvage 收集器,就是采用该算法进行回收的。

改进思路:由于新生代都是朝生夕死的,所以不需要 1:1 划分内存空间,可以将内存划分为一块较大的 Eden 和两块较小的 Suvivor 空间。每次使用 Eden 和其中一块 Survivor。当回收的时候,将 Eden 和 Survivor 中还活着的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才使用过的 Suevivor 空间。其中 Eden 和 Suevivor 的大小比例是8:1。

缺点是需要老年代进行分配担保,如果第二块的 Survovor 空间不够的时候,需要对老年代进行垃圾回收,然后存储新生代的对象,这些新生代当然会直接进入老年代。

分代收集算法

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)(方法区)。

老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

img

新生代(Young Generation)

a) 一般情况下,所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

b) 新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在 Eden 区中生成。回收时先将 eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将 survivor0 区和 survivor1 区交换,即保持survivor1区为空, 如此往复。

c) 当 survivor1 区不足以存放 eden 和 survivor0 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

d) 新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等 Eden 区满了才触发)。

老年代(Old Generation)

a) 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此可以认为年老代中存放的都是一些生命周期较长的对象。

b) 大对象直接进入老年代。

c) 内存比新生代也大很多(大概比例是1:2,即新生代占堆内存的 1 / 3,老年代占堆内存的 2 / 3),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

永久代(Permanent Generation)

用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如使用反射、动态代理、CGLib 等 bytecode 框架时,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区

垃圾收集类型

  • Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

  • Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。

    导致Full GC 的原因:老年代被写满、永久代(Perm)被写满、 System.gc() 被显式调用等。

垃圾收集器

下图是 HotSpot 虚拟机包含的所有收集器:

在这里插入图片描述

  • Serial 收集器(复制算法)
    新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
  • Serial Old 收集器(标记-整理算法)
    老年代单线程收集器,Serial收集器的老年代版本。
  • ParNew 收集器(停止-复制算法) 
    新生代收集器,可以认为是 Serial 收集器的多线程版本, 在多核 CPU 环境下有着比 Serial 更好的表现。
  • Parallel Scavenge收集器(停止-复制算法)
    并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量 = 用户线程时间 / ( 用户线程时间+ GC线程时间 )。适合后台应用等对交互相应要求不高的场景。是 server 级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
  • Parallel Old 收集器(停止-复制算法)
    Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先。
  • CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
    高并发、低停顿,追求最短GC回收停顿时间,CPU 占用比较高,响应时间快,停顿时间短,多核 CPU 追求高响应时间的选择。
  • G1是 JDK 1.7中正式投入使用的用于取代 CMS 的压缩回收器。它虽然没有在物理上隔断新生代与老生代,但是仍然属于分代垃圾回收器。G1 仍然会区分年轻代与老年代,年轻代依然分有 Eden 区与 Survivor 区。

在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值