文章目录
- 1、实现思路
- 2、SWT
- 3、GC算法
- 4、标记清除算法Mark Sweep GC
- 5、复制算法Copying GC
- 5、标记整理算法Mark Compact GC
- 6、分代算法Generational GC
- 7、分代的整体流程
- 8、为什么分代GC把堆内存分为年轻代和老年代?📕
1、实现思路
Java实现垃圾回收的步骤:
- 根据GC Root对象可达性分析,将内存中对象标记为存活的、可回收的
- 处理可回收的对象,释放空间
2、SWT
GC是在一个单独的线程,但不管JVM用哪种算法,都会存在一个阶段需要停止所有的用户线程,称Stop The World(STW),SWT大,用户用起来自然卡。
感受下SWT:
public class StopTheWorld {
public static void main(String[] args) {
/**
* 启动用户线程和GC线程
* 查看不同阶段用户线程的执行时间
*/
new PrintTimeThread().start();
new ClearThread().start();
}
}
/**
* 模拟用户代码,这里直接打印这段代码的执行耗时
*/
class PrintTimeThread extends Thread {
@SneakyThrows //lombok的try..catch
@Override
public void run() {
long begin = System.currentTimeMillis();
while (true) {
long now = System.currentTimeMillis();
System.out.println(now - begin);
begin = now;
Thread.sleep(100);
}
}
}
/**
* 模拟GC线程
*/
class ClearThread extends Thread {
@SneakyThrows
@Override
public void run() {
List<byte[]> list = new LinkedList<>();
while (true) {
//存80个100M后就删除里面byte对象的强引用,垃圾回收释放
if(list.size() >= 80){
list.clear();
}
list.add(new byte[1024 * 1024 * 100]);
Thread.sleep(100);
}
}
}
添加JVM参数,使用分代回收的垃圾回收器,输出GC详细信息,并限制堆最大10G:
-XX:+UseSerialGC -Xmx10g -verbose:gc
运行发现用户线程本来100ms左右的事儿,有时候会被拖到2000ms以上:
3、GC算法
对象回收算法的评价标准:
- 吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间),值越大,性能越高
- 最大暂停时间:SWT的最大值
- 堆的使用效率:如复制算法只用一半空间
以上三个指标,不可兼得。各个算法各有长处,对应着不同的适用场景。
4、标记清除算法Mark Sweep GC
实现:
- 从GC Root List开始,遍历引用链,找到可达对象,并标记
- 清除没标记的对象
优点:
- 实现简单,只需给对象维护个标记位
缺点:
- 导致内存碎片化:从原本连续的内存空间,摘掉一些被回收的,得到一些碎片。如下回收了4+3+2,却连个5字节的对象都创建不了
- 分配速度慢:由于内存碎片化,需要维护一个空闲链表记录可用空间,新对象来了每次都得往后遍历,找出一块合适大小的地儿安置
5、复制算法Copying GC
实现:
- 堆内存一分为二,一半叫From,一半叫To
- 新对象来了往From安置
- GC时,把From的存活对象Copy到To
- 清掉From,From和To名字互换,原来的To做为新的From安置新new的对象
完整例子:
- 开始状态:
-
GC开始,把GC Root对象和可达的对象搬到To空间
-
清掉From空间,并把原来的To改为From空间
一句话:将存活的对象搬运到另一块空间,清理掉当前空间,互换名字
优点:
- 解决了内存碎片化:往To搬的时候,按连续地址往过码
- 吞吐相比下面的标记整理算法要高:只需遍历一次存活对象。但不如标记-清除算法,因为后者不用给对象搬家
缺点:
- 堆内存使用率低:安置新对象只能用50%的堆空间,另一半得留着To
5、标记整理算法Mark Compact GC
也称标记压缩,用来解决标记清除算法的内存碎片化缺点。
实现:
- 从GC Root开始,遍历标记可达对象
- 将可达的存活对象移动到堆的一端,清掉非存活的
优点:
- 无内存碎片化问题:比标记清除多了一步整理
- 堆内存利用率比复制算法高
缺点:
- 理解阶段性能不高,得看整理阶段的实现算法
6、分代算法Generational GC
组合使用了上面的几种算法,被主流使用。分代即把内存分为年轻代和老年代:
关于这几块空间的大小设置:
Demo:
public class Gc {
@SneakyThrows
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int count = 0;
while (true) {
System.in.read();
System.out.println(++count);
list.add(new Byte[1024 * 1024]);
}
}
}
对应JVM的参数:
-XX:+UseSerialGC -Xms60m -Xmn20m -Xmx60m -XX:SurvivorRatio=3 -XX:+PrintGDetails
粗略计算:老年代60m - 20m = 40m,Eden除以随便一块s区 = 3,则Eden:s0:s1 = 12:4:4,使用阿尔萨斯执行memory验证:
7、分代的整体流程
- 新new的对象,安置到堆的年轻代的伊甸园区
- 伊甸园区满了以后,触发GC,仅是年轻代的GC(Minor GC、Young GC)
- 把Eden的存活对象放入S1(To),Eden区被清空(复制算法)
- 互换名,S0做为To,S1做为From,再安置新对象,直到Eden和From满
- 再次触发Minor GC,Eden和From存活对象放入S0,其余清掉回收(每次GC能活下来的,记录年龄,+1)
- 对象GC年龄到达阈值(最大15,对象头里放着,默认值和垃圾回收器有关),晋升到老年代。(一直活着就别在From和To之间来回搬了)
- 老年代最后也满了,新new的对象进来,先Minor GC,还是不足,再Full GC,对整个堆进行垃圾回收,此时的STW时间就比Minor GC时的SWT长一些了
- Full GC后,无法回收老年代对象,再往老年代放,就OOM
补充:如果现在新生代已经满了,Minor GC还是满,再来对象,尽管新生代有的对象没到达年龄阈值,也会被搬到老年代
8、为什么分代GC把堆内存分为年轻代和老年代?📕
从上面的GC分代流程就可以看到一个最核心的点:只给年轻代GC,STW时间更短了,这是明摆着的好处。
答案:
- 分代GC下,可以只进行Minor GC,不用每次Full GC,STW时间短
- 开发者可以通过调整年轻代和老年代的比例来适应不同的服务场景,提高性能(对象用完即丢的,生命短的多,可以调大年轻代,目的就是少STW,非STW的,也能Minor就别Full)
- 年轻代和老年代可以选择使用不同的算法,年轻代通常用复制算法、老年代则用标记清除或者标记整理
补充:
- 很多对象都是new完很快就可以回收,比如一个个Vo
- 老年代存放一直用的对象,比如Spring容器里的一些Bean
- JVM默认设置下,新生代空间远小于老年代