JVM内存模型及垃圾回收

目录

内存模型的划分

垃圾回收算法

引用计数法

根搜索算法

标记-清除算法

复制算法

标记-整理算法

增量算法

分代收集算法

gc(garbage collection)垃圾收集器

1.Serial收集器

2 ParNew

3 Parallel收集器(侧重于吞吐量)

4. CMS收集器(降低停顿时间,更快的响应时间)

5 G1收集器(Garbage First)

 参考资料


内存模型的划分

通俗的来讲,jvm主要分为5个部分  程序计数器、虚拟机枝、本地方法枝、 Java 堆、 方法区, 引用大佬总结的概括程序计数器用于存放下一条运行的指令,虚拟机栈和本地方法栈用于存放函数调用堆栈信息, Java 堆用于存放 Java 程序运行时所需的对象等数据,方法区用于存放程序的类元数据信息 。 

       JVM内存模型及垃圾回收_第1张图片

程序计数器: 是一块很小的内存空间,每个线程私有,可以看作当前线程程序执行的字节码的行号提示器。

: 线程私有存储空间,访问速度仅次于寄存器,栈里面的存储单位为,栈帧, 栈帧对应着方法, 存放着方法的 局部变量表,操作数栈,常量池引用,方法返回地址。

本地方法栈:用于管理本地方法的调用,本地方法一般都是由C语言编写,调用本地方法,都会使用本地方法栈, 而不是像 自己定义的方法那样在虚拟机的栈中创建栈帧,而是java栈动态连接到本地方法栈。

方法区:   线程之间共享的,储存类的类型信息,常量池,方法信息,JIT编译后的代码等数据等, jdk1.7后  对方法区,的位置都由改变, jdk 1.7以前, 常量池在永久代中, 从1.7 以后就搬入到堆中, 方便对其进行垃圾回收, jdk1.8以后永久代就被元空间取代;

: 所有线程之间共享的,首先是默认分为 新生代和老年代,默认占比是1:2,新生代有分为eden区和survivor(分为 from 和to) 默认比例为 8:1;

垃圾回收算法

垃圾标记算法有,引用计数法,和根搜索算法(可达性分析)等,常见的垃圾收集算法有 标记-清除算法( Mark-Sweep ),复制算法(Copying ),标记-整理算法(Mark-Compact )

引用计数法

引用计数法( Reference Counting)在 GC 执行垃圾回收之前,首先需要区分出内存中哪些
是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象, GC 才会在执行垃圾回
收时,释放掉其所占用的内存空间 , 因此这个过程我们可以称为垃圾标记阶段。

对于对象A来说,只要有任意一个对象引用了A,A的引用计数器就加1, 当引用失效时,计数器就减一,当对象A的计数器的值为0时,就可以被回收,  但是有个明显的问题,当存在对象A,对象B,它们之间互相引用,导致引用计数都不为0,就不能被删除掉。

根搜索算法

以根对象集合为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中
的存活对象都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味着该对象己经死亡,便可以将其标记为垃圾对象。在根搜索算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象, 当对象被标记为不可达,并不意味着会被马上清除掉,一个对象的死亡,至少要经历两次标记过程。 第一次是执行对象的finalize()将对象放入F-Queue 中, 之后再进行一次判断,才会被清除掉

当对象放入F-queue时候,只要将对象,与任何引用关联上,就可以使对象重新存活,

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive() {
        System.out.println("yes, i am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救自己       
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它       
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead");
        }
        // 下面这段代码与上面的完全相同,但是这次自救却失败了       
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它       
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead");
        }
    }
}

(对象的finalize方法只会被执行一次,第二次对象就没有被放入到F-queue 直接被回收了)执行的结果为:

finalize mehtod executed!
yes, i am still alive
no, i am dead

标记-清除算法

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

JVM内存模型及垃圾回收_第2张图片

复制算法

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

JVM内存模型及垃圾回收_第3张图片

标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动整理压缩,然后直接清理掉端边界以外的内存。

JVM内存模型及垃圾回收_第4张图片

 

增量算法

垃圾回收的过程都需要暂停应用程序,如果垃圾回收暂停了很长时间,就会影响用户的体验,因此增量算法的思想,就是用多线程的方式,一边进行标记-清除,复制,清理垃圾, 一边执行应用程序, 这样就减少了系统的停顿时间;

分代收集算法

将堆细分为年轻代,和老年代,根据不同代中对象的使用情况不用, 分别使用不同垃圾收集器, 年轻代中,对象大多数属于朝生夕死的,使用效率较高的复制算法, 老年代对象大多是都是经历过多次垃圾回收幸存的对象, 在加上老年代回收性价要比年轻代低,使用标记整理算法。(看到分代收集器,是不是突然明白了,为啥jvm的堆,要分为年轻代,老年代,年轻代 有为啥有两个survivor区 )

gc(garbage collection)垃圾收集器

垃圾收集器主要的工作分为两个部分,分别位内存的动态分配和垃圾回收,   一个垃圾收集器的选择在于业务的注重点, 

垃圾收集器侧重点在于吞吐量与响应时间,两个只能优先满足其一。

1.Serial收集器

 

串行收集器是最古老,最稳定以及效率高的收集器

可能会产生较长的停顿,只使用一个线程去回收

-XX:+UseSerialGC

  • 新生代、老年代使用串行回收
  • 新生代复制算法
  • 老年代标记-压缩

JVM内存模型及垃圾回收_第5张图片

2 ParNew

-XX:+UseParNewGC(new代表新生代,所以适用于新生代)

  • 新生代并行
  • 老年代串行

Serial收集器新生代的并行版本  在新生代回收时使用复制算法 多线程,需要多核支持

-XX:ParallelGCThreads 限制线程数量

JVM内存模型及垃圾回收_第6张图片

3 Parallel收集器(侧重于吞吐量)

类似ParNew 

新生代复制算法 

老年代标记-压缩 

更加关注吞吐量 

-XX:+UseParallelGC  

  • 使用Parallel收集器+ 老年代串行

-XX:+UseParallelOldGC 

  • 使用Parallel收集器+ 老年代并行

JVM内存模型及垃圾回收_第7张图片

-XX:MaxGCPauseMills

最大停顿时间,单位毫秒 GC尽力保证回收时间不超过设定值

 -XX:GCTimeRatio 

0-100的取值范围 垃圾收集时间占总时间的比 默认99,即最大允许1%时间做GC

这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优

4. CMS收集器(降低停顿时间,更快的响应时间)

Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)

使用标记-清除算法   并发阶段会降低吞吐量(停顿时间减少,吞吐量降低)

老年代收集器(新生代使用ParNew)

-XX:+UseConcMarkSweepGC

CMS运行过程 

JVM内存模型及垃圾回收_第8张图片

1. 初始标记(会产生全局停顿,标记所有根可达对象) 

2. 并发标记(和用户线程一起,将不可达对象标记为垃圾对象) 

3. 重新标记 (会产生全局停顿,因为在并发标记阶段,用户线程的同时进行,不能确保当时的所有垃圾对象引用关系没有发生改变,需要重新检查) 

4. 并发清除(和用户线程一起,进行内存回收) 

1为啥初始阶段和重新标记阶段,都需要stop-the-word(全局停顿), 如果不进行全局停顿的话,和用户线程同时执行的话,当前所标记的对象,不一定能代表当前时刻, 所以必须进行停顿

2 这里就能很明显的看出,为什么CMS要使用标记清除而不是标记整理,如果使用标记整理,需要多对象的内存位置进行改变,这样程序就很难继续执行。但是标记清除会产生大量内存碎片,不利于内存分配。 

 

CMS收集器特点:

尽可能降低停顿  会影响系统整体吞吐量和性能

比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半

清理不彻底   因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理

因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够) 

-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值

如果不幸内存预留空间不够,就会引起concurrent mode failure一旦 concurrent mode failure产生,将使用串行收集器作为后备。

CMS也提供了整理碎片的参数:

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理

整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction  

设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads 

设定CMS的线程数量(一般情况约等于可用CPU数量)

CMS的提出是想改善GC的停顿时间,在GC过程中的确做到了减少GC时间,但是同样导致产生大量内存碎片,又需要消耗大量时间去整理碎片,从本质上并没有改善时间。 

5 G1收集器(Garbage First)

G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

在解释G1的过程之前,先介绍一下G1 中特有的概念

分区(region)

cms中存在一个问题,就是老年代使用的是标记清除算法,导致会产生很多内存碎片,暂停时间不可控制等问题,G1 提出region的概念

JVM内存模型及垃圾回收_第9张图片

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。
年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

收集集合(CSet)

一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。

 已记忆集合(RSet)

每个region特有的,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

G1收集器的收集活动主要有四种操作:

 1 新生代垃圾收集:

JVM内存模型及垃圾回收_第10张图片

1Eden区耗尽的时候就会触发新生代收集,新生代垃圾收集会对整个新生代进行回收,新生代垃圾收集期间,整个应用STW新生代垃圾收集是由多线程并发执行的,新生代收集结束后依然存活的对象,会被拷贝到一个新的Survivor分区,或者是老年代。

2 并发收集

JVM内存模型及垃圾回收_第11张图片

G1设计了一个标记阈值,它描述的是总体Java堆大小的百分比,默认值是45,这个值可以通过命令-XX:InitiatingHeapOccupancyPercent(IHOP)来调整,一旦达到这个阈值就回触发一次并发收集周期。注意:这里的百分比是针对整个堆大小的百分比,而CMS中的CMSInitiatingOccupancyFraction命令选型是针对老年代的百分比

 1、初始标记(initial-mark),在这个阶段,应用会经历STW,通常初始标记阶段会跟一次新生代收集一起进行,换句话说——既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作。
在新生代垃圾收集中进行初始标记的工作,会让停顿时间稍微长一点,并且会增加CPU的开销。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象;
2、根分区扫描(root-region-scan),这个过程不需要暂停应用,在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。
survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集,如果扫描根分区时,新生代的空间恰好用尽,新生代垃圾收集必须等待根分区扫描结束才能完成。如果在日志中发现根分区扫描和新生代收集的日志交替出现,就说明当前应用需要调优。
3、并发标记阶段(concurrent-mark),并发标记阶段是多线程的,我们可以通过-XX:ConcGCThreads来设置并发线程数,默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数(-XX:ParallelGCThreads)的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;
记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。
4、重新标记阶段(remarking),重新标记阶段是最后一个标记阶段,需要暂停整个应用,G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。
5、清理阶段(cleanup),清理阶段真正回收的内存很小,截止到这个阶段,G1垃圾收集器主要是标记处哪些老年代分区可以回收,将老年代按照它们的存活度(liveness)从小到大排列。

混合式垃圾收集

JVM内存模型及垃圾回收_第12张图片

混合收集只会回收部分(默认1/8)老年代,混合收集会执行多次,一直运行到(几乎)所有标记点老年代分区都被回收,在这之后就会恢复到常规的新生代垃圾收集周期。当整个堆的使用率超过指定的百分比时,G1 GC会启动新一轮的并发标记周期。在混合收集周期中,对于要回收的分区,会将该分区中存活的数据拷贝到另一个分区,这也是为什么G1收集器最终出现碎片化的频率比CMS收集器小得多的原因——以这种方式回收对象,实际上伴随着针对当前分区的压缩。

Full GC

Gl 的 Full GC 和 Serial GC 的 Full GC 采用的是同一种算法 。 Full GC 会对整个 Java 堆进行整理 。 Gl 的 Full GC 是单线程的,会引起较长的停顿时间,因此 Gl 的设计目标是减少 Full GC的发生次数。

G1调优:

1. 关键参数项

  • -XX:+UseG1GC,告诉JVM使用G1垃圾收集器

  • -XX:MaxGCPauseMillis=200,设置GC暂停时间的目标最大值,这是个柔性的目标,JVM会尽力达到这个目标

  • -XX:INitiatingHeapOccupancyPercent=45,如果整个堆的使用率超过这个值,G1会触发一次并发周期。记住这里针对的是整个堆空间的比例,而不是某个分代的比例。

2. 最佳实践

不要设置年轻代的大小

通过-Xmn显式设置年轻代的大小,会干扰G1收集器的默认行为:

  • G1不再以设定的暂停时间为目标,换句话说,如果设置了年轻代的大小,就无法实现自适应的调整来达到指定的暂停时间这个目标

  • G1不能按需扩大或缩小年轻代的大小

响应时间度量

不要根据平均响应时间(ART)来设置-XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标

3. G1 GC的参数选项

参数名 含义 默认值
-XX:+UseG1GC 使用G1收集器 JDK1.8中还需要显式指定
-XX:MaxGCPauseMillis=n 设置一个期望的最大GC暂停时间,这是一个柔性的目标,JVM会尽力去达到这个目标 200
-XX:InitiatingHeapOccupancyPercent=n 当整个堆的空间使用百分比超过这个值时,就会触发一次并发收集周期,记住是整个堆 45
-XX:NewRatio=n 新生代和老年代的比例 2
-XX:SurvivorRatio=n Eden空间和Survivor空间的比例 8
-XX:MaxTenuringThreshold=n 对象在新生代中经历的最多的新生代收集,或者说最大的岁数 G1中是15
-XX:ParallelGCThreads=n 设置垃圾收集器的并行阶段的垃圾收集线程数 不同的平台有不同的值
-XX:ConcGCThreads=n 设置垃圾收集器并发执行GC的线程数 n一般是ParallelGCThreads的四分之一
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出(疏散失败)的风险。默认值是 10%。增加或减少这个值,请确保对总的 Java 堆调整相同的量 10
-XX:G1HeapRegionSize=n 分区的大小 堆内存大小的1/2000,单位是MB,值是2的幂,范围是1MB到32MB之间
-XX:G1HeapWastePercent=n 设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,JavaHotSpotVM不会启动混合垃圾回收周期(注意,这个参数可以用于调整混合收集的频率)。 JDK1.8是5
-XX:G1MixedGCCountTarget=8 设置并发周期后需要执行多少次混合收集,如果混合收集中STW的时间过长,可以考虑增大这个参数。(注意:这个可以用来调整每次混合收集中回收掉老年代分区的多少,即调节混合收集的停顿时间) 8
-XX:G1MixedGCLiveThresholdPercent=n 一个分区是否会被放入mix GC的CSet的阈值。对于一个分区来说,它的存活对象率如果超过这个比例,则改分区不会被列入mixed gc的CSet中

JDK1.6和1.7是65,JDK1.8是85

 

 

 

 

 参考资料

参考了 《深入理解JVM & G1 GC》和 《深入理解Java虚拟机_JVM高级特性与最佳实践》  安利一下

博客:https://blog.csdn.net/xiaoye319/article/details/85252195,转载了博客的部分内容

 

 

 

 

你可能感兴趣的:(JVM内存模型及垃圾回收)