GC垃圾回收
文章目录
一、如何判断对象可以回收
1. 引用计数法
-
当一个对象被引用时,计数器加一;不被引用时,计数器减一;当计数器为0时表示没有被引用,可以回收
-
存在的问题
- A对象引用B对象,B对象引用A对象,没有人引用二者,造成循环引用,无法被回收
2. 可达性分析算法
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的所有对象,看是否能够沿着GC Root对象(根对象)为起点的引用链找到某一对象,若找不到,表示可以回收;即判断某一对象有没有被根对象直接或间接的调用,若没有,表示可以被回收
- 根对象表示不可以被垃圾回收的对象
- 如Object类、HashMap类、String类等
- 正在被用来加锁的对象(还要用其来解锁)
- 栈帧内的变量等数据
二、垃圾回收算法
1. 标记清除
-
过程
-
如果判断数据可以回收就对其进行标记
-
将标记的可以被回收的数据进行清除
-
清除并不是直接将内存清除,而是将被清除的空间的起始地址与结束地址放入空闲内存表中;等到再次分配内存时,直接去表中寻找地址分配
-
-
优点
- 清除时无需直接将内存清空,只需要记录下起始地址即可,故效率较高
-
缺点
- 由于被回收的地址不连续,会产生内存碎片(单个大小不满足分配,但加起来满足分配)
2. 标记整理
-
可以解决标记清除算法的内存碎片问题
-
过程
-
如果判断数据可以回收就对其进行标记
-
将标记的数据清空
-
将不需要回收的数据进行整理,移动成一整块的内存空间
-
-
优点
- 不会产生内存碎片
-
缺点
- 整理的过程需要改变内存地址,效率较低
3. 复制
-
过程
-
将内存空间复制一份,分为FROM和TO(TO为新复制的)
-
如果判断FROM中的数据可以回收就对其进行标记
-
将FROM中不需要回收的数据移动至TO,移动的同时对其进行整理,没有产生内存碎片
-
在FROM中将标记的内容清空
-
将FROM和TO的内容交换
-
-
优点
- 不会产生内存碎片
-
缺点
- 需要双倍的内存空间
JVM中同时使用了上述三种垃圾回收算法,根据不同的情况采用不同的算法
三、分代回收
1. 定义
-
分代回收使用了新生代和老年代两部分空间
- 新生代的垃圾回收称为Minor GC,使用复制算法
- 老年代的垃圾回收称为Full GC(会连同新生代一起清理),使用标记整理 / 清除算法
-
老年代中存放的是需要长期使用的数据,回收不频繁
-
新生代中存放的是使用完之后即可丢弃的数据,回收频繁
-
根据不同的区域采用不同的垃圾回收算法,提高效率
2. 工作机制
-
新创建的对象放入新生代的伊甸园
-
当伊甸园没有足够的空间存入新对象时,会触发Minor GC,触发此GC会引发stop the world,即暂停其他的用户线程,直到垃圾回收线程结束
-
采用垃圾回收的复制算法,存活的对象移动到幸存区To中,并将其寿命+1;伊甸园标记的对象被清空
-
交换幸存区FROM和TO的内容
-
伊甸园此时为空,继续重复上述过程将新对象放入伊甸园
-
当伊甸园没有足够的空间存入新对象时,会触发Minor GC;并检查幸存区FROM中有无继续需要保留的对象
-
采用垃圾回收的复制算法,伊甸园存活的对象移动到幸存区To中,并将其寿命+1;幸存区FROM中继续保留的对象移动到幸存区To中,并将其寿命+1
-
将伊甸园和幸存区FROM中标记的对象清空
-
交换幸存区FROM和TO的内容
-
重复以上步骤
-
当幸存区FROM中的某一对象的寿命达到了某一值(比如15),则认为它长期使用,将其移动至老年代
-
当老年代内存不够时,触发Full GC,此GC也会引起stop the world,且引发的时间更长
-
如果执行Full GC之后内存空间仍不够,会报错OutOfMemoryError:Java heap space
3. 大对象
当对象的占用空间大于伊甸园的内存空间时,会直接将此对象放入老年代
4. 相关虚拟机参数
四、垃圾回收器
1. 分类
-
串行垃圾回收器(SerialGC)
- 单线程,执行垃圾回收的过程中,需要暂停其他所有线程
- 适用于堆内存较小的情况,通常为个人电脑
-
并行垃圾回收器(ParallelGC)
- 多线程,多个垃圾回收的线程可以同时进行
- 适用于多核cpu、堆内存较大的情况
- 如吞吐量优先垃圾回收器:尽可能让单位时间内,Stop the world总时间最短
-
并发垃圾回收器(CMS)
- 多线程,垃圾回收线程和用户线程可以同时进行
- 适用于多核cpu、堆内存较大的情况
- 如响应时间优先垃圾回收器:尽可能让单次Stop the world的时间最短
-
G1垃圾回收器
2. 串行垃圾回收器
-
工作流程
-
安全点作用
- 让所有线程在安全点停下来,因为垃圾回收的过程中会导致对象的地址进行改变,设置了安全点,就不会有别的线程影响当前的线程;否则,如果正在移动某一对象的地址时别的线程正好引用这个对象,地址找不到,会发生错误
3. 并行垃圾回收器
-
工作流程
4. 并发垃圾回收器
-
采用标记清除算法,会产生内存碎片
-
工作流程
-
初始标记和重新标记的过程中,会触发Stop the world
-
初始标记阶段
- 在这个阶段中,程序中所有的工作线程都将会因为“stop—the—World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快
-
并发标记阶段
- 从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,用户线程可以与垃圾收集线程一起并发运行
-
重新标记阶段
- 由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
- 重新标记的过程中多个线程同时进行,但需要暂停所有用户线程
- 重新标记的原因是并发标记的过程中,别的线程可能会调用已经被标记的数据,对GC产生影响
- 对象被误清理的情况
- 并发标记阶段,用户线程断掉了某一对象A的所有引用,JVM会标记此对象为待清理,但用户线程又让某一对象引用A,此时A对象被引用着,但仍然会被清理
- 解决方法:当某个对象的引用被用户线程修改而发生变化时,将此对象添加到队列中,重新标记时会从此队列中取出对象,再次判断对象是否被引用,若被引用则不会清理
-
并发清除阶段
- 此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
5. G1垃圾回收器
1)简介
- 适用场景
-
属于并发垃圾回收器
-
适合超大堆内存,会将堆划分为多个大小相等的Region(区域),每个区域都可当作伊甸园、幸存区、老年代
-
整体上是标记整理算法,两个区域之间是复制算法
-
- 相关虚拟机参数
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time (设置暂停时间)
2)G1垃圾回收阶段
- 三者是顺时针循环的过程,最先执行新生代回收
- 三个阶段分别是新生代回收、新生代回收 + 并发标记、混合回收
i. 新生代回收
-
会触发STW,暂停所有用户线程,使用多线程进行垃圾回收;会进行初始标记
-
使用E表示伊甸园区域、S表示幸存区区域、O表示老年代区域
-
新创建的对象会放入伊甸园
-
伊甸园内存不够时执行复制算法,将需要保留的对象移至幸存区
-
伊甸园对象移至幸存区、幸存区对象移至幸存区或老年代(与之前新生代回收过程相同)
-
新生代回收会回收伊甸园区域和幸存区区域(新生代中只有二者)
ii. 并发标记
-
针对老年代的区域
-
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
-xx:InitiatingHeapoccupancyPercent=percent (默认45%)
-
实时回收
- 若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收
-
并发清理
- 识别并清理完全空闲的区域
iii. 混合回收 (新生代回收 + 老年代回收)
-
会对E、S、O三个区域进行全面的回收
-
老年代回收采用的依然是复制算法
-
为了达到所设定的暂停时间,不会对所有的O区域执行复制算法,只对回收价值比较高的区域进行回收
3)跨代引用
-
新生代回收的过程中通过卡表与remembered set机制加快新生代回收速度
-
新生代进行初始标记的过程中需要找到所有的根对象,根对象中的一部分来自老年代,通过遍历老年代找到所有的根对象显然效率比较低
-
卡表机制
-
将老年代区域划分为一张卡表,每张卡表分为一个个的card
-
如果某一个card中的对象引用了新生代的对象,则将此card标记为脏卡;好处是遍历GC Root时无需遍历整个老年代,只需要关心脏卡的区域即可
-
-
remembered set机制
-
伊甸园的区域有remembered set机制,记录卡表中的被引用的脏卡
-
进行新生代回收时,通过remembered set读取到伊甸园保留的脏卡,从而可以通过脏卡遍历到所有的根对象,减少了GC Root的遍历时间
-