1. 判断对象可以被回收

1.1 引用计数法

给对象添加一个引用计数器,每当用一个地方引用它时,计数器加一;当引用失效时,计数器减一,计数器为0的对象就是不可能再被使用的。

存在的弊端:循环引用,如下图中A对象和B对象仅仅只是被彼此引用了,而不会被其它地方所引用,但是存在引用又无法进行回收,这就存在无辜的内存消耗。
在这里插入图片描述

1.2 可达性分析算法

通过一系列称为“GC Root” 的对象作为起始点,从这些节点往下找,搜索走过的路程称为引用链,当一个对到GC Roots没有任何引用链时,说明这个对象是不可用的,那么他们就会被判断成可回收的对象。

1.2.1 GC Root 对象

Java中的GC Root 对象:

  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象

使用MAT查看运行项目的GC Root 对象:
在这里插入图片描述

1.3 四种引用

  1. 强引用:最普遍存在的,只要沿着GC Root 对象的引用链找到就不会被垃圾回收;
  2. 软引用:还有用但非必须的对象,没有强引用对象引用它,并且内存不足时就可能会被回收;
  3. 弱引用:非必需对象的,没有强引用引用时触发垃圾回收就会被回收;
  4. 虚引用:有无虚引用存在不会对对象生存时间造成影响,也无法通过虚引用获取对象实例;虚引用的目的是能够在对象被回收时收到一个系统通知。比如:ByteBuffer,GC回收直接内存,需要引用Cleaner对象来触发Unsafe类的释放内存方法;
  5. 终结器引用:所有对象都有一个finallize()方法,当没有强引用对象引用时,JVM会帮助当前对象创建虚拟机引用,当前对象被垃圾回收时,会把终结器引用加入到引用队列,再由其它线程找到这个终结器引用对象,再调用当前对象的finallize()方法,再次GC时会把当前对象回收。

如图:
A1对象:被B对象和A对象两个GC Root 引用不会被垃圾回收;只有引用都断掉才能会被回收。
A2对象:被C对象间接软引用也被B对象引用;如果B对象断开引用,触发垃圾回收时,且内存不够就会回收A2对象。
A3对象:被C对象间接弱引用也被B对象引用;如果B对象断开引用,触发垃圾回收时会回收A3对象。
在这里插入图片描述

1.4 引用队列

如果配合了引用队列来使用软引用和弱引用,当软(弱)引用对象所引用的对象被回收时软(弱)对象就会进入到以用队列中去,引用队列可以释放软(弱引用)对象。
虚引用和终结器引用都必须要搭配引用队列来使用,当被虚引用引用的对象回收时,虚引用对象就会进入到引用队列中,

2. 垃圾回收算法

2.1 标记清除

分为“标记”和“清除”两个阶段:首先要标记出所有需要回收的对象,在标记完成后统一回收所有的被标记对象。

特点:

  1. 速度快:清除过程只需要记录内存的起始和结束地址;
  2. 容易产生内存碎片:清除产生的内存空间不是连续的,无法放下内存较大对象。

2.2 标记整理

分为“标记” 和 “整理部分”,整理之后会将存活的对象都向一端移动,然后直接清理掉边界以外的内存,从而解决内存碎片的问题。

特点:

  1. 没有内存碎片
  2. 效率低:移动对象需要考虑对象引用相关问题。

2.3 复制

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

把FROM区存活的对象复制到TO区,并且在复制过程中也会进行整理,不会产生碎片内存,等到清空完成之后交换FROM和TO,

清理之前:
在这里插入图片描述
完成清理之后(FROM已经换成了TO区):
在这里插入图片描述
特点:

  1. 不会产生碎片
  2. 占用双倍的内存空间

3. 分代回收

JVM将堆内存划分为如下区域:新生代存放存活时间比较短的对象,老年代存放比较有价值的对象;针对不同的区域采用不同的算法,对垃圾回收施行更有效的管理。
在这里插入图片描述

3.1 分代回收过程

创建对象时,会放到伊甸园:
对象创建
伊甸园无法再容纳新对象时,会触发一次新生代的垃圾回收Minor GC,采用可达性分析,将伊甸园和FROM中存活的对象使用copy复制到TO区,并且将这些对象的寿命加一,交换FROM和TO的位置,将伊甸园中的已经死亡的对象清空。
Minor GC 会引发 Stop the world,将其它用户的线程都暂停,等到复制和回收都完成之后(因为涉及复制相关,如果对象移动就可能会造成混乱),再启动其它线程。
在这里插入图片描述
这样就可以再向伊甸园存放新的对象,然后再进行上诉过程,当一个对象经历过多次Minor GC 之后,他的“寿命”达到一个固定的阈值(最大值15),就会将这个对象晋升到老年代,
在这里插入图片描述
当老年代空间不足会先尝试触发Minor GC,如果空间仍然不足,会触发Full GCFull GCStop the world ** 时间会更长,会将整个新生代和老年代都进行清理。
在这里插入图片描述
如果触发了
Full GC** 之后堆空间仍然不足就会抛出堆内存溢出异常java.lang.OutOfMemoryError: Java heap space

3.2 GC相关参数

含义参数
堆容量最大值-Xmx
堆容量初始值-Xms
新生代容量-Xmn
幸存区比例(动态)-XX:InitialSurvivorRatio=ratio
幸存区比例-XX:SurvivorRatio=ratio
晋升阈值-XX:MaxTenuringThreshold=threshold
晋升详情-XX:+PrintTenuringDistribution
GC详情-XX:+PrintGCDetails -verbose:gc
Full GC 前 Minor GC-XX:+ScavengeBeforeFullGC

3.3 实战演示GC过程

添加参数:-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
堆内存最大为20M,新生代为10M,那么老年代也是10M
在这里插入图片描述

		ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);

当新生代内存不够时会触发Minor GC
在这里插入图片描述

 		ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);

当新生代的内存不足够放下一个大对象时,对象会直接晋升到老年代中,不会引起新生代的GC
在这里插入图片描述

 		ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);

当老年代和新生代都满的时候会先触发Minor GC 然后再触发 Full GC 如果此时还不能创建新对象就会抛出异常。
在这里插入图片描述

  new Thread(()->{
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

其它的线程创建对象时如果堆内存溢出不会影响主线程

4. 垃圾回收器

4.1 串行

单线程垃圾回收器,运行时其它线程都暂停,
适合堆内存较小的个人电脑。

开启串行回收器:-XX:+UseSerialGC = Serial + SerialOld,新生代采用copy算法,老年代采用复制-整理算法。

普通线程在一个安全点停下来,保证对象的地址不会改变,普通线程进入阻塞状态,等待垃圾回收线程回收完毕,在变回到运行态。
在这里插入图片描述

4.2 吞吐量优先

多线程垃圾回收器,在单位时间Stop the world的时间最短。指一定时间内触发的GC的总和时间最短。
适合堆内存较大,多核CPU来支持,服务器电脑

在JDK1.8下默认使用的是吞吐量优先垃圾回收器,等线程达到安全点后会开启多个线程同时回收,默认情况下线程的数量是基于计算机本身的核心数的,可以通过:

  1. -XX:ParallelGCThreads = n :来设置线程数量;
  2. -XX:+UseAdaptiveSizePolicy:开启自适应调整新生代大小;
  3. -XX:GCTimeRatio=ratio:调整垃圾回收时间和总时间的占比,计算公式:1 / (1 + ratio),默认99,一般使用19,如果达不到这个目标就会尝试调整堆的大小,从而达到吞吐量的提高。
  4. -XX:MaxGCPauseMills = nms:最大暂停毫秒数,默认200ms。
    在这里插入图片描述

4.3 响应时间优先

多线程垃圾回收器,使单次回收Stop the world的时间尽可能的短。工作在老年代
适合堆内存较大,多核CPU来支持。

  1. -XX:+UseConMarkSweepGC ~ -XX+UseParNewGC ~ SerialOld,开启方式,用户线程和垃圾回收线程同时进行。
  2. -XX:ParalleGCThreads=n ~ -XX:ConGCThreads = threads:指定并发线程并行线程,一般设置并行线程为并发的1/4来执行垃圾回收,其它的线程正常运行。
  3. -XX:CMSInitiatingOccupancyFraction=precent:设置CMS垃圾回收的内存占比,;因为在垃圾回收过程中会产生新的垃圾称为浮动垃圾,需要预留出来一部分空间给浮动垃圾。
  4. -XX:+CMSScavengeBeforeRemark:在重新标记之前,对新生代做一次垃圾回收;在重新标记过程中,需要对整个堆内存进行扫描,新生代中对象较多,并且有部分需要回收,对他们进行扫描会影响性能。

工作流程:当老年代内存不足时会将所有的线程在安全点停下来,垃圾回收线程进行初始标记,标记完成之后其它线程可以重新运行,垃圾回收线程并发标记,等到并发标记完成之后,需要Stop the World,所有线程一块进行标记,标记完成之后,垃圾回收线程执行并发清理,其它线程同时运行。

因为需要部分线程来进行垃圾回收,所以对程序的吞吐量有一定的影响。
在这里插入图片描述

5. G1垃圾回收器

Garbage First ,不过一般发音称Garbage One。
在JDK9中为JVM默认的垃圾回收器,取代了之前的CMS垃圾回收器。
开启G1:-XX:+UseG1GC

使用场景:

  1. 注重吞吐量和低延迟,默认的暂停目标是200ms;最大暂停时间:-XX:MaxGCPauseMill=time
  2. 超大堆内存,将堆内存划分为多个相等的区域;设置区域大小:-XX:G1HeapRegionSize=n,值只能是2的倍数(或者1)。
  3. 整体上是标记+整理算法,两个区域之间是复制算法。

5.1 垃圾回收阶段

三个垃圾回收阶段是循环运行的。
在这里插入图片描述

5.1.1 Young Collection

新生代垃圾回收,G1将堆内存划分称大小相等的多个区域,每个区域都可以独立作为伊甸园、FROM、TO。当作为伊甸园的区域被占满是,会触发Young Collection,会触发STW

在这里插入图片描述

当触发了新生代回收时,会将幸存的对象复制到幸存区。

在这里插入图片描述

再次触发Young Collection时,年龄不够的存活对象会被复制到其它的幸存区 ,当对象存活时间足够久之后会晋升到老年代。

在这里插入图片描述

5.1.2 Young Collection + Concurrent Mark

新生代垃圾回收时会进行GC Root初始标记;老年代的堆空间比例达到一定阈值时,会进行并发标记。
配置阈值:-XX:InitiatingHeapOccupancyPercent=n默认为45%。

在这里插入图片描述

5.1.3 Mixed Collection

混合回收会对伊甸园、幸存区、老年代进行统一回收。
新生代的晋升和回收和上方回收一样。
不同的是针对于老年代的回收,如果暂停时间不足,G1会基于设置的最大暂停时间有选择的回收老年代中的垃圾对象,因此被称为Garbage First。相反如果时间足够就会回收全部。
设置最大暂停时间:-XX:MaxGCPauseMillis=ms

在这里插入图片描述

5.1.4 Young Collection 跨代引用

可达性分析需要找到其对象的根对象,如果新生代对象的根对象位于老年代,但老年代存活对象非常多,全部遍历效率低。因此采用了一个Card Table 技术,如果其中有对象引用了新生代对象,就标记为脏卡,这样在进行GC Root时直接去脏卡里面找,提高效率。
伊甸园中通过Remember Set 记录脏卡的位置,变更时也会异步更新Remember Set

在这里插入图片描述
脏卡引用:
在这里插入图片描述

5.1.5 Remark

如图普通标记阶段所示:

  1. 黑色表示重新标记完成,并且有对象以用它;
  2. 灰色正在标记处理;
  3. 白色暂未处理;
  4. 处理之后如果没有引用也是白色;
  5. 全部处理完毕会清除白色的对象。
    在这里插入图片描述
    但由于标记是异步处理的,如果在异步处理过程中,对象的引用地址改变,会造成意外回收被强引用的对象。

Remark重新标记阶段:只要对象的引用发生了变化,就会执行写屏障pre-write barrier指令,将对象加入到队列satb_mark_queue中,再等待处理。等到整个并发标记结束了之后,会进行重新标记,暂停所有的用户线程,把队列中的对象取出重新检查。

5.1.6 字符串去重

在JDK 8u20版本之后引入了字符串去重。将所有新分配的字符串放入一个队列,当新生代回收时,G1并发检查是否有字符串重复,如果值一样就引用相同的。
开启:-XX:+UseStringDeduplication

优点:节省内存;
缺点:占用CPU时间,新生代垃圾回收时间增加。

5.1.7 类卸载

在JDK 8u40之后,当所有的对象都经过并发标记后,如果一个类加载器的所有类都不再使用,就卸载它的所有类。
开启:-XX:+ClassUnloadingWithConcurrentMark

5.1.8 回收巨型对象

当一个对象大于分区一般时,称为巨型对象;G1不对拷贝巨型对象,且会优先考虑回收。如果老年代中有没有“卡”引用了巨型对象,该巨型对象就会被新生代回收时回收掉。

如图中H:
在这里插入图片描述
没有老年代引用的巨型对象:
在这里插入图片描述

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐