Java基础 — JVM垃圾回收

一、jvm内存

在这里插入图片描述

二、虚拟机回收机制

在这里插入图片描述1. 对象回收算法

虚拟机对象回收算法主要包括引用计数法和可达性分析法两种 :

(1) 引用计数算法

定义:引用计数算法就是给对象添加一个引用计数器,每当有一个地方引用该对象的时候就会+1,相反当失去一个引用的时候就-1,当引用数为0的时候也就说明这个对象不在被使用就可以被回收。

这种算法实现简单,判定效率也很高,在大部分情况下它是一个不错的算法,但是唯一的缺点就是在两个对象相互引用的时候那么他们的引用就不会为0,那么GC也就无法回收他们,因此当前主流的Java虚拟机基本上没有选用引用计数算法来管理内存。

(2) 可达性分析算法

定义:这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连是,则表明该对象不可达,则可以进行内存回收。如下图,Object5、6、7是可以被回收的。

“GC Roots”的对象包括

    虚拟机栈(栈帧中的本地变量表)中引用的对象;
    方法区中类静态属性引用的对象;
    方法区中常量引用的对象;
    本地方法栈中JNI(即一般说的Native方法)引用的对象;

其实对于可达性分析算法,并不是发生一次GC就会对不在GC Roots相连接的引用链的对象进行回收,有些对象可能处于“死缓”状态,对于这些对象至少要经历两次标记过程。如果对象在进行可达性分析后发现没有在引用链上,它将会被第一次标记并进行筛选,筛选的条件是此对象是否有必要执行finalize方法,有必要执行finalize方法的对象就属于“死缓”状态的对象。

行finalize依据

当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过一次,这两种情况只要满足其一,都表示没必要执行finalize()方法。

对于被判定有必要执行finalize()方法的对象,GC会将该对象放到一个叫F-Queue的队列之中,并由虚拟机自动创建的一个Finalizer线程去执行各个对象的finalize()方法,在该过程即会进行第二次标记过程,如果某些对象存在“自我救赎”现象,则会将这些对象移出“即将回收”的集合,那对于没有移出的对象,基本上就真正回收了。

2.垃圾收集算法

(1)标记-清除算法

标记-清除(Mark-Sweep)算法可以算是最基础的垃圾收集算法,该算法主要分为“标记”和“清除”两个阶段。先标记可以被清除的对象,然后统一回收被标记要清除的对象,这个标记过程采用的就是可达性分析算法。

标记清除算法最大的缺点是在垃圾回收之后会产生大量的内存碎片,而如果内存碎片多了,当我们再创建一个占用内存比较大的对象时就没有足够的内存来分配,那么这个时候虚拟机就还要再次触发GC来清理内存后来给新的对象分配内存。

(2)复制算法

为了解决效率问题及标记清除算法的缺点,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,触发GC操作将存活的对象复制到另一个区域当中,然后再把使用过的内存空间一次清理掉。这样使得每次都对整个半区进行内存回收,内存分配也不用考虑内存碎片的问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

这种算法最大的缺点是将原有内存分成了两块,每次只能使用区中一块,也就是损失了50%的内存空间,代价有点大。

(3)标记-整理算法

复制算法在对象存活率较高的情况下需要进行较多的复制操作,这样效率也会变低,更关键的是还需要浪费50%的内存空间,为了解决这些问题,于是“标记-整理”(Mark-Compact)算法就出来了,标记过程仍然使用可达性算法来判断,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理边界以外的内存。

(4)分代收集算法

主流虚拟机都是采用“分代收集”(Generational Collection)算法,根据不同区域特点采用特定的回收算法策略,一般把Java堆分为新生代和老年代,然后根据不同年代的特点采用适当的收集算法。例如在新生代,一般情况下每次垃圾收集都会有大量的对象死去,只有少量的存活,这时候一般使用复制算法,而对于年老代,由于对象存活率高,没有额外空间对它跟配担保,因此一般采用“标记-清理”或“标记-整理”算法来实现。

3.垃圾收集算法

(1)年轻代

年轻代包括Eden区和Survivor区。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,Minor GC非常频繁,新生代采用复制算法,一般回收速度也比较快。因为采用复制算法,所以年轻代分为三部分:1个Eden区和2个Survivor区(分别叫From和To),默认比例为8:1。

GC的流程如下:

a.在GC开始时,对象只存在于Eden区和From区,To是空的。
b.紧接着,Eden区中所有存活的对象都会被复制到To,而在From区中,仍存活的对象会根据他们的年龄值来决定去向。
c.年龄达到一定值(年龄值可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中。
d. 没有达到阈值的对象会被复制到To区。
这次GC后,Eden区和From区已经被清空。
此时From和To会交换他们的角色。也就是新的To就是上次GC前的From,新的From就是上次GC前的To。不管怎样,都会保证名为To的Survivor区域是空的。
Minor GC会一直重复这样的过程,直到To区被填满,To区被填满之后,会将所有对象移动到年老代中。

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

*有关年轻代的JVM参数

-XX:NewSize和-XX:MaxNewSize:用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

-XX:SurvivorRatio:用于设置Eden和其中一个Survivor的比值。

-XX:+PrintTenuringDistribution:这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold:用于设置晋升到老年代的对象年龄的最小值和最大值。

(2) 年老代。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。

Major GC的速度一般会比Minor GC慢10倍以上。

大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

*年老代涉及的参数

-XX:PretenureSizeThreshold令大于这个设置值的对象直接在老年代分配。

(3) 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

4.G1垃圾回收器

G1是一款面向服务端应用的垃圾收集器,HotSpot团队赋予它的使命是未来可以替换掉CMS收集器,G1收集器的特点:

  • 并行与并发:G1可以充分利用多CPU,多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,熬过多次GC的旧对象以获得更好的收集效果
  • 空间整合:G1在整体上采用标记-整理算法,局部采用复制算法,使得它在运行期间不会产生内存空间碎片,收集后能提供规整的可用内存,这种特性有利于程序长时间运行,分配大对象的时候不会因为无法找到连续内存空间而提前触发下一次GC
  • 可预测的停顿:这是CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型

G1收集器将整个Java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但它们已经不再是物理隔离了,它们是一部分不需要连续的集合

G1收集器之所以能够建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1收集器的运作步骤:

  1. 初始标记:仅仅只是标记一下GC
    Roots能够直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但耗时很短
  2. 并发标记:从GC Root开始对堆中的对象进行可达性分析,找出存活的对象,这一阶段耗时较长,但可与用户程序并发执行
  3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录,可并行执行
  4. 筛选回收:该阶段可以做到与用户程序一起并发执行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【江湖】三津

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值