JVM的GC垃圾回收(Java Garbage Collection)

JVM的GC垃圾回收(Java Garbage Collection)

释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

GC两种类型

轻GC(普通的GC、Minor GC),重GC(全局GC、Full GC)

Minor GC的过程

首先垃圾回收主要发生在堆内存中,堆的结构如图所示。
在这里插入图片描述
Survivor 0 Space,幸存者0区,也叫from区;Survivor 1 Space,幸存者1区,也叫to区。
其中,from区和to区的区分不是固定的,是互相交换的,意思是说,在每次GC之后,两者会进行交换,谁空谁就是to区。

  • Eden Space、from复制到to,年龄+1。
    首先,当Eden Space满时,会触发第一次GC,把还活着的对象拷贝到from区。而当Eden Space再次触发GC时,会扫描Eden Space和from,对这两个区进行垃圾回收,经过此次回收后依旧存活的对象,则直接复制到to区(如果对象的年龄已经达到老年的标准,则移动至老年代区),同时把这些对象的年龄+1。
  • 清空Eden Space、from
    然后,清空Eden Space和from中的对象,此时的from是空的。
  • from和to互换
    最后,from和to进行互换,原from成为下一次GC时的to,原to成为下一次GC时的from。部分对象会在from和to中来回进行交换复制,如果交换15次(由JVM参数MaxTenuringThreshold决定,默认15),最终依旧存活的对象就会移动至老年代。
    总结一句话,GC之后有交换,谁空谁是to。

Full GC发生的条件

对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下情况会发生 full GC:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

内存的分配策略

对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
可以设置JVM参数 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和Survivor 之间的大量内存复制。

长期存活的对象进入老年代

通过参数 -XX:MaxTenuringThreshold 可以设置对象进入老年代的年龄阈值。对象在 Survivor 中每经过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。

动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

空间分配担保

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

垃圾判断算法

引用计数法、可达性分析法

引用计数法

引用计数法是为对象添加一个引用计数器,然后用一块额外的内存区域来存储每个对象被引用的次数,当对象每有一个地方引用它时,那我们对该对象的引用计数就会加1,反之每有一个引用失效时,我们对该对象的引用计数就会减1, 当对象的被引用次数为0时,那么我们可以认为这个对象是不会被再次使用了,通过这种方式我们能快速直观的定位到这些可回收的对象,从而进行清理。

缺点:无法解决循环引用的问题:引用计数法虽然很直观高效,但是通过引用计数法是没办法扫描到一种特殊情况下的“可回收”对象,这种特殊情况就是对象循环引用的时候,比如A对象引用了B,B对象引用了A,除此之外他们两个没有被任何其他对象引用,那么其实这部分对象也属于“可回收”的对象,但是通过引用计数法是没办法定位的。

可达性分析法

可达性分析法是通过以所有的“GC Roots”对象为出发点,如果无法通过GC Roots的引用追踪到的对象,那我们认为这些对象就不会再次被使用了,现在主流的程序语言都是通过可达性分析法来判断对象是否存活的。
在这里插入图片描述
哪些对象对象我们称之为"GC Roots"对象呢? 当然普通的对象肯定是不行的,如果要作为GC Roots 对象那么它自身肯定得满足一个条件,那就是他自己一定在很长一段时间内都不会被GC 回收掉。那么只有满足这个条件的对象才可能作为GC Roots了,GC Roots的类型大致如下:

1、虚拟机栈中的本地变量所引用的对象。
2、方法区中静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法中(Native方法)引用的对象。
5、虚拟机内部的引用对象(类记载器、基本数据对应的Class对象,异常对象)。
6、所有被同步锁(Synchronnized)持有的对象。
7、描述虚拟机内部情况的对象(如 JMXBean、JVMTI中注册的回调、本地缓存代码)。
8、垃圾搜集器所引用的对象。

垃圾回收算法

标记清除、标记复制、标记整理、分代收集

标记清除算法

标记清除算法是先找到内存里的存活对象并对其进行标记,然后统一把未标记的对象统一的清理。
在这里插入图片描述

优点

  • 不需要额外的空间、存活对象较多的情况下比较高效、适用于老年代

不足

  • 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
  • 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)

标记复制算法

标记清除算法最大问题是会造成空间碎片,并且只适合需要回收的对象比较少的场景,那么针对这个问题就衍生了标记复制算法,标记复制算法专门针对这两个问题进行了解决。标记清除算法的关注点在可回收的对象身上,而标记复制算法的关注点则放在了存活的对象身上,通过把存活的对象放挪到一个固定的区域,然后对其他区域的对象进行统一清理。

首先它把年轻代内存划分出三块区域,一块用于存放新创建的对象叫Eden区,另外两块则用于存放存活的对象分别叫 S1区和S2区。回收的时候会有两种情况,一种是把Eden和S1区的存活对象复制到S2区,第二种是把Eden和S2区的存活对象复制到S1区,也就是说S1区和S2区这两块区域同时只会有一块使用,通过这种方式保证始终会有一块空白的q区域用于下次GC时存放存活的对象,而且原来的区域不需要考虑保留存活的对象,所以可以直接一次性清除所有对象,这要既简单直接同时也保证了清除后的内存区域的内存连续性。

在这里插入图片描述

优点

  • 标记复制算法解决了标记清除算法的空间碎片问题,并且采用移动存活对象的方式,每次清除针对的都是一整块内存,所以清除可回收对象的效率也比较高,但因为要移动对象所以这里会耗费一部分时间,所以标记复制算法的效率还是会低于标记清除算法。
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 存活对象较少的情况下比较高效,适用于新生代

不足

  • 会浪费一部分空间:总是会有一块空闲的内存区域是利用不到的,这也造成了资源的浪费。
  • 存活对象多会非常耗时:因为复制移动对象的过程是比较耗时的,不仅需要移动对象本身还需要修改使用了这些对象的引用地址,所以当存活对象多的场景会非常耗时,这也提示我们标记复制法比较适合存活对象较少的场景。

标记压缩(整理)算法

标记复制算法是完美的补齐了标记清除算法的短板,即解决了空间碎片的问题,又适合使用在大部分对象都是可回收的场景。 但是问题是标记复制算法并不适用于存活对象多的场景,如果使用标记清除算法来处理,那么空间碎片又是一个无法忍受的问题。有了具体的场景和问题,所以就有了标记整理算法,它是专门针对于存活对象多的情况下进行垃圾收集当然同时需要要避免产生空间碎片。

标记整理算法分为标记和整理两个阶段,标记阶段会先把存活的对象和可回收的对象标记出来;标记完之后就是进行整理了,这个阶段会把存活的对象往内存的一端移动,移动完对象后再清除可回收的对象。
在这里插入图片描述优点:标记整理法最大的特点就是解决了标记清除法的空间碎片问题,同时也不至于像标记复制法需要空闲的内存空间,所以它非常适合存活对象多的场景。

缺点:标记整理法是三种垃圾回收算法中性能最低的一种,因为标记整理法在移动对象的时候不仅需要移动对象,还要额外的维护对象的引用的地址,这个过程可能要对内存经过几次的扫描定位才能完成,做的事情越多那么必然消耗的时间也越多。

分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

GC算法总结

内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度,指扫描空间的次数)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

年轻代:

  • 存活率底
  • 复制算法!

老年代:

  • 区域大,存活率高
  • 标记清除(内存碎片不是太多)+标记压缩混合 实现

(注:本文仅作学习记录使用,若图片侵权,请联系)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值