Java虚拟机之垃圾回收
1. 判断对象可以被回收1.1 引用计数法给对象添加一个引用计数器,每当用一个地方引用它时,计数器加一;当引用失效时,计数器减一,计数器为0的对象就是不可能再被使用的。存在的弊端:循环引用,如下图中A对象和B对象仅仅只是被彼此引用了,而不会被其它地方所引用,但是存在引用又无法进行回收,这就存在无辜的内存消耗。1.2 可达性分析算法通过一系列称为“GC Root” 的对象作为起始点,从这些节点往下找
1. 判断对象可以被回收
1.1 引用计数法
给对象添加一个引用计数器,每当用一个地方引用它时,计数器加一;当引用失效时,计数器减一,计数器为0的对象就是不可能再被使用的。
存在的弊端:循环引用,如下图中A对象和B对象仅仅只是被彼此引用了,而不会被其它地方所引用,但是存在引用又无法进行回收,这就存在无辜的内存消耗。
1.2 可达性分析算法
通过一系列称为“GC Root” 的对象作为起始点,从这些节点往下找,搜索走过的路程称为引用链,当一个对到GC Roots没有任何引用链时,说明这个对象是不可用的,那么他们就会被判断成可回收的对象。
1.2.1 GC Root 对象
Java中的GC Root 对象:
- 虚拟机栈中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中引用的对象;
使用MAT查看运行项目的GC Root 对象:
1.3 四种引用
- 强引用:最普遍存在的,只要沿着GC Root 对象的引用链找到就不会被垃圾回收;
- 软引用:还有用但非必须的对象,没有强引用对象引用它,并且内存不足时就可能会被回收;
- 弱引用:非必需对象的,没有强引用引用时触发垃圾回收就会被回收;
- 虚引用:有无虚引用存在不会对对象生存时间造成影响,也无法通过虚引用获取对象实例;虚引用的目的是能够在对象被回收时收到一个系统通知。比如:
ByteBuffer
,GC回收直接内存,需要引用Cleaner
对象来触发Unsafe
类的释放内存方法; - 终结器引用:所有对象都有一个
finallize()
方法,当没有强引用对象引用时,JVM会帮助当前对象创建虚拟机引用,当前对象被垃圾回收时,会把终结器引用加入到引用队列,再由其它线程找到这个终结器引用对象,再调用当前对象的finallize()
方法,再次GC时会把当前对象回收。
如图:
A1对象:被B对象和A对象两个GC Root 引用不会被垃圾回收;只有引用都断掉才能会被回收。
A2对象:被C对象间接软引用也被B对象引用;如果B对象断开引用,触发垃圾回收时,且内存不够就会回收A2对象。
A3对象:被C对象间接弱引用也被B对象引用;如果B对象断开引用,触发垃圾回收时会回收A3对象。
1.4 引用队列
如果配合了引用队列来使用软引用和弱引用,当软(弱)引用对象所引用的对象被回收时软(弱)对象就会进入到以用队列中去,引用队列可以释放软(弱引用)对象。
虚引用和终结器引用都必须要搭配引用队列来使用,当被虚引用引用的对象回收时,虚引用对象就会进入到引用队列中,
2. 垃圾回收算法
2.1 标记清除
分为“标记”和“清除”两个阶段:首先要标记出所有需要回收的对象,在标记完成后统一回收所有的被标记对象。
特点:
- 速度快:清除过程只需要记录内存的起始和结束地址;
- 容易产生内存碎片:清除产生的内存空间不是连续的,无法放下内存较大对象。
2.2 标记整理
分为“标记” 和 “整理部分”,整理之后会将存活的对象都向一端移动,然后直接清理掉边界以外的内存,从而解决内存碎片的问题。
特点:
- 没有内存碎片;
- 效率低:移动对象需要考虑对象引用相关问题。
2.3 复制
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
把FROM区存活的对象复制到TO区,并且在复制过程中也会进行整理,不会产生碎片内存,等到清空完成之后交换FROM和TO,
清理之前:
完成清理之后(FROM已经换成了TO区):
特点:
- 不会产生碎片;
- 占用双倍的内存空间。
3. 分代回收
JVM将堆内存划分为如下区域:新生代存放存活时间比较短的对象,老年代存放比较有价值的对象;针对不同的区域采用不同的算法,对垃圾回收施行更有效的管理。
3.1 分代回收过程
创建对象时,会放到伊甸园:
伊甸园无法再容纳新对象时,会触发一次新生代的垃圾回收Minor GC,采用可达性分析,将伊甸园和FROM中存活的对象使用copy
复制到TO区,并且将这些对象的寿命加一,交换FROM和TO的位置,将伊甸园中的已经死亡的对象清空。
Minor GC 会引发 Stop the world,将其它用户的线程都暂停,等到复制和回收都完成之后(因为涉及复制相关,如果对象移动就可能会造成混乱),再启动其它线程。
这样就可以再向伊甸园存放新的对象,然后再进行上诉过程,当一个对象经历过多次Minor GC 之后,他的“寿命”达到一个固定的阈值(最大值15),就会将这个对象晋升到老年代,
当老年代空间不足会先尝试触发Minor GC,如果空间仍然不足,会触发Full GC;Full GC 的 Stop 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下默认使用的是吞吐量优先垃圾回收器,等线程达到安全点后会开启多个线程同时回收,默认情况下线程的数量是基于计算机本身的核心数的,可以通过:
-XX:ParallelGCThreads = n
:来设置线程数量;-XX:+UseAdaptiveSizePolicy
:开启自适应调整新生代大小;-XX:GCTimeRatio=ratio
:调整垃圾回收时间和总时间的占比,计算公式:1 / (1 + ratio),默认99,一般使用19,如果达不到这个目标就会尝试调整堆的大小,从而达到吞吐量的提高。-XX:MaxGCPauseMills = nms
:最大暂停毫秒数,默认200ms。
4.3 响应时间优先
多线程垃圾回收器,使单次回收
Stop the world
的时间尽可能的短。工作在老年代
适合堆内存较大,多核CPU来支持。
-XX:+UseConMarkSweepGC ~ -XX+UseParNewGC ~ SerialOld
,开启方式,用户线程和垃圾回收线程同时进行。-XX:ParalleGCThreads=n ~ -XX:ConGCThreads = threads
:指定并发线程和并行线程,一般设置并行线程为并发的1/4来执行垃圾回收,其它的线程正常运行。-XX:CMSInitiatingOccupancyFraction=precent
:设置CMS垃圾回收的内存占比,;因为在垃圾回收过程中会产生新的垃圾称为浮动垃圾,需要预留出来一部分空间给浮动垃圾。-XX:+CMSScavengeBeforeRemark
:在重新标记之前,对新生代做一次垃圾回收;在重新标记过程中,需要对整个堆内存进行扫描,新生代中对象较多,并且有部分需要回收,对他们进行扫描会影响性能。
工作流程:当老年代内存不足时会将所有的线程在安全点停下来,垃圾回收线程进行初始标记,标记完成之后其它线程可以重新运行,垃圾回收线程并发标记,等到并发标记完成之后,需要Stop the World
,所有线程一块进行标记,标记完成之后,垃圾回收线程执行并发清理,其它线程同时运行。
因为需要部分线程来进行垃圾回收,所以对程序的吞吐量有一定的影响。
5. G1垃圾回收器
Garbage First ,不过一般发音称Garbage One。
在JDK9中为JVM默认的垃圾回收器,取代了之前的CMS垃圾回收器。
开启G1:-XX:+UseG1GC
使用场景:
- 注重吞吐量和低延迟,默认的暂停目标是200ms;最大暂停时间:
-XX:MaxGCPauseMill=time
; - 超大堆内存,将堆内存划分为多个相等的区域;设置区域大小:
-XX:G1HeapRegionSize=n
,值只能是2的倍数(或者1)。 - 整体上是标记+整理算法,两个区域之间是复制算法。
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
如图普通标记阶段所示:
- 黑色表示重新标记完成,并且有对象以用它;
- 灰色正在标记处理;
- 白色暂未处理;
- 处理之后如果没有引用也是白色;
- 全部处理完毕会清除白色的对象。
但由于标记是异步处理的,如果在异步处理过程中,对象的引用地址改变,会造成意外回收被强引用的对象。
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:
没有老年代引用的巨型对象:
更多推荐
所有评论(0)