简介: 每个 java 开发同学不论是日常工作中还是面试里,都会遇到 JDK、JVM 和 GC 的问题。本文会从以下 10 个问题为切入点,带着大家一起全面理解一下 JVM 的方方面面。
每个 java 开发同学不论是日常工作中还是面试里,都会遇到 JDK、JVM 和 GC 的问题。本文会从以下 10 个问题为切入点,带着大家一起全面理解一下 JVM 的方方面面。
- JVM、JRE 和 JDK 的区别和分割
- JVM 是什么?以及它的次要作用
- JVM 的外围性能有哪些
- 类加载机制和过程
- 运行时数据区的逻辑构造
- JVM 的内存模型
- 如何确定对象是垃圾
- 垃圾收集的算法有哪些
- 各种问世的垃圾收集器
- JVM 调优的参数配置
上一篇文章结尾时咱们谈到,就 JVM 的设计规范,从应用用处角度 JVM 的内存大体的分为:线程公有内存区 和 线程共享内存区。
线程公有内存区在类加载器编译某个 class 文件时就确定了执行时须要的“程序计数器”和“虚构栈帧”等所需的空间,并且会随同着以后执行线程的产生而产生,执行线程的沦亡而沦亡,因而“线程公有内存区”并不需要思考内存治理和垃圾回收的问题。线程共享内存区在虚拟机启动时创立,被所有线程共享,是 Java 虚拟机所治理内存中最应该关注的和最大的一块。首先咱们来一起看一下“线程共享内存区”的内存模型是什么样的?
6、JVM 的内存模型
如图所示,JVM 的内存构造分为堆和非堆两大块区域。
- 其中“非堆”就是上篇文章咱们提到的办法区或叫元数据区,用来存储 class 类信息的。
- 而“堆”是用来存储 JVM 各线程执行期间所创立的实例对象或数组的。堆辨别为两大块,一个是 Old 区,一个是 Young 区。Young 辨别为两大块,一个是 Survivor 区(S0+S1),一块是 Eden 区 S0 和 S1 一样大,也能够叫 From 和 To。
之所以这样划分,设计者的目标无非就是为了内存治理,也就是咱们说的垃圾回收。那么什么样的对象是垃圾?垃圾回收算法有哪些?目前罕用的垃圾回收器又有哪些?这篇文章咱们一起弄清楚这些问题和知识点。
7、如何确定一个对象是垃圾?
要想进行垃圾回收,得先晓得什么样的对象是垃圾。目前确认对象是否为垃圾的算法次要有两种:援用计数法和可达性分析法。
- 1、援用计数法:在对象中增加了一个援用计数器,当有中央援用这个对象时,援用计数器的值就加 1,当援用生效的时候,援用计数器的值就减 1。当援用计数器的值为 0 时,JVM 就开始回收这个对象。
对于某个对象而言,只有应用程序中持有该对象的援用,就阐明该对象不是垃圾,如果一个对象没有任何指针对其援用,它就是垃圾。这种办法尽管很简略、高效,然而 JVM 个别不会抉择这个办法,因为这个办法会呈现一个弊病:当对象之间互相指向时,两个对象的援用计数器的值都会加 1,而因为两个对象时互相指向,所以援用不会生效,这样 JVM 就无奈回收。
- 2、可达性分析法:针对援用计数算法的弊病,JVM 采纳了另一种算法,以一些 ”GC Roots” 的对象作为起始点向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到 GC Roots 没有任何援用链相连时,则证实此对象是不可用的,即能够进行垃圾回收。否则,证实这个对象有用,不是垃圾。
上图中的 obj7 和 obj8 尽管它们相互援用,但从 GC Roots 登程这两个对象不可达,所以会被标记为垃圾。JVM 会把以下几类对象作为 GC Roots:
- (1)虚拟机栈(栈帧中本地变量表)中援用的对象;
- (2)办法区中类动态属性援用的对象;
- (3)办法区中常量援用的对象;
- (4)本地办法栈中 JNI(Native 办法)援用的对象。
注:在可达性剖析算法中不可达的对象,并不是间接被回收,这时它们处于缓刑状态,至多须要进行两次标记才会确定该对象是否被回收:
第一次标记:如果对象在进行可达性剖析后发现没有与 GC Roots 相连接的援用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()办法(该办法可将此对象与 GC Roots 建立联系)。在 finalize()办法中没有从新与援用链建设关联关系的,将被进行第二次标记。
第二次标记胜利的对象将真的会被回收,如果对象在 finalize()办法中从新与援用链建设了关联关系,那么将会逃离本次回收,持续存活。
8、垃圾收集的算法有哪些
晓得了如何 JVM 确定哪些对象是垃圾后,上面咱们来看一下,面对这些垃圾对象,JVM 的回收算法都有哪些。
1、标记 - 革除算法(Mark-Sweep)
- 第一步“标记”,如下图所示把堆里所有的对象都扫描一遍,找出哪些是垃圾须要回收的对象,并且把它们标记进去。
- 第二步“革除”,把第一步标记为“UnReference Object”(无援用或不可达)的对象革除掉,开释内存空间。
这种算法的毛病次要有两点:
(1)标记和革除两个过程都比拟耗时,效率不高
(2)革除后会产生大量不间断的内存碎片空间,碎片空间太多可能会导致当程序后续须要创立较大对象时,无奈找到足够间断的内存空间而不得不再次触发垃圾回收。
2、标记 - 复制算法(Mark-Copying)
将内存划分为两块区域,每次应用其中一块,当其中一块用满,触发垃圾回收的时候,将存活的对象复制到另一块下来,而后把之前应用的那一块进行格式化,一次性革除洁净。
(革除前)
(革除后)
“标记 - 复制”算法的毛病不言而喻,就是内存空间利用率低。
3、标记 - 整顿算法(Mark-Compact)
标记整顿算法标记过程依然与 ” 标记 - 革除 ” 算法一样,然而后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉端边界以外的内存。
将所有存活的对象向一边挪动,清理掉存活边界以外的全副内存空间。
联合这三种算法咱们能够看到,
- “标记 - 复制”算法的长处是回收效率高,但空间利用率上有肯定的节约。
- 而“标记 - 整顿”算法因为须要向一侧挪动等一系列操作,其效率绝对低一些,但对内存空间治理上非常优异。
- 因而,“标记 - 复制”算法实用于那些生命周期短、回收频率高的内存对象,
- 而标记 - 整顿”算法实用于那些生命周期长、回收频率低,但重视回收一次内存空间失去足够开释的场景。
因而 JVM 的设计者将 JVM 的堆内存,分为了两大块区域 Young 区和 Old 区,Young 区存储的就是那些生命周期短,应用一两次就不再应用的对象,回收一次基本上该区域十之有八的对象全副被回收清理掉,因而 Young 区采纳的垃圾回收算法也就是“标记 - 复制”算法。Old 区存储的是那些生命周期长,通过屡次回收后依然存活的对象,就把它们放到 Old 区中,平时不再去判断这些对象的可达性,直到 Old 区不够用为止,再进行一次对立的回收,开释出足够的间断的内存空间。
9、各种问世的垃圾收集器
鉴于 Young 区和 Old 区须要采纳不同的垃圾回收算法,因而在 JVM 的整个垃圾收集器的演进各个时代里,针对 Young 区和 Old 区每个时代都是不同的垃圾收集机制。从 JDK1.3 开始到目前,JVM 垃圾收集器的演进大体分为四个时代:串行时代、并行时代、并发时代和 G1 时代。
1、串行时代:Serial(Young 区)+ Serial Old(Old 区)
JDK3(1.3)的时候,大略是 2000 年左右,那个时代根本计算机都是单核一个 CPU 的,因而垃圾回收最后的设计实现也是基于单核单线程工作的。并且垃圾回收线程的执行绝对于失常业务线程执行来说还是 STW(stop the world)的,应用一个 CPU 或者一条收集线程去实现垃圾收集工作,这个线程执行的时候其它线程须要进行。
串行收集器采纳单线程 stop-the-world 的形式进行收集。当内存不足时,串行 GC 设置进展标识,待所有线程都进入平安点 (Safepoint) 时,利用线程暂停,串行 GC 开始工作,采纳单线程形式回收空间并整顿内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能无效利用多核优势。因而,串行收集器特地适宜堆内存不高、单核甚至双核 CPU 的场合。
2、并行时代:Parallel Scavenge(Young 区)+ Parallel Old(Old 区)
并行收集器是以关注吞吐量为指标的垃圾收集器,也是 server 模式下的默认收集器配置,对吞吐量的关注次要体现在年老代 Parallel Scavenge 收集器上。
并行收集器与串行收集器工作模式类似,都是 stop-the-world 形式,只是暂停时并行地进行垃圾收集。年老代采纳复制算法,老年代采纳标记 - 整顿,在回收的同时还会对内存进行压缩。关注吞吐量次要指年老代的 Parallel Scavenge 收集器,通过两个指标参数 -XX:MaxGCPauseMills 和 -XX:GCTimeRatio,调整新生代空间大小,来升高 GC 触发的频率。并行收集器适宜对吞吐量要求远远高于提早要求的场景,并且在满足最差延时的状况下,并行收集器将提供最佳的吞吐量。
3、并发时代:CMS(Old 区)
并发标记革除 (CMS) 是以关注提早为指标、非常优良的垃圾回收算法,CMS 是针对 Old 区的垃圾回收实现。
老年代 CMS 每个收集周期都要经验:初始标记、并发标记、从新标记、并发革除。其中,初始标记以 STW 的形式标记所有的根对象;并发标记则同利用线程一起并行,标记出根对象的可达门路;在进行垃圾回收前,CMS 再以一个 STW 进行从新标记,标记那些由 mutator 线程 (指引起数据变动的线程,即利用线程) 批改而可能错过的可达对象;最初失去的不可达对象将在并发革除阶段进行回收。值得注意的是,初始标记和从新标记都已优化为多线程执行。CMS 非常适合堆内存大、CPU 核数多的服务器端利用,也是 G1 呈现之前大型利用的首选收集器。
– 但 CMS 有以下两个缺点:
- (1)因为它是标记 - 革除不是标记 - 整顿,因而会产生内存碎片,Old 区会随着工夫的推移而究竟被耗尽或产生无奈调配大对象的状况。最初不得不通过底层的担保机制(CMS 背地有串行的回收作为兜底)进行一次 Full GC,并进行内存压缩。
- (2)因为标记和革除都是通利用线程并发进行,两类线程同时执行时会减少堆内存的占用,一旦某一时刻内存不够用,就会触发底层担保机制,又采纳串行回收进行一次 STW 的垃圾回收。
4、G1 时代:Garbage First
G1 收集器时代,Java 堆的内存布局与就与其余收集器有很大差异,它将整个 Java 堆划分为多个大小相等的独立区域(Region),尽管还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不须要间断)的汇合。
如上图所示,每一个 Region(分区)大小都是一样的,1~32M 之间的数值,但必须是 2 的指数。设置 Region 大小通过以下参数:-XX:G1HeapRegionSize=M。
G1 收集器的原理或特点次要有以下三点:
(1)内存逻辑上仍保留的分代的概念,每一个 Region 同一时间要么被标记为新生代,要么被标记为老年代,要么处于闲暇;
(2)整体上采纳了“标记 - 整顿算法”,不会产生内存碎片
(3)可预测的进展,G1 整体采纳的策略是“筛选回收”,也就是回收前会对各个待回收的 Region 的回收价值和老本进行排序,依据 G1 配置所冀望的回收工夫,抉择排在后面的几个 Region 进行回收。
其实之所以叫 G1(Garbage First)就是因为它优先选择回收垃圾比拟多的 Region 分区。
整体 G1 的垃圾回收工作步骤分为:初始标记、并发标记、最终标记和筛选回收。
5、ZGC:Zero GC
这篇文章简略提一下这个最新问世的垃圾收集器,之所以叫“Zero GC”是因为它谋求的是更低的 GC 进展工夫,谋求的指标是:反对 TB 级堆内存(最大 4T)、最大 GC 进展 10ms。JDK11 新引入的 ZGC 收集器,不论是物理上还是逻辑上,ZGC 中曾经不存在新老年代的概念了会分为一个个 page,当进行 GC 操作时会对 page 进行压缩,因而没有碎片问题。因为其是 JDK11 和只能在 64 位的 linux 上应用,因而目前用得还比拟少。
结语
以上总体两篇文章七千字,就是我从 JVM 的作用、设计框架到 JVM 内存治理的整体的体系化了解。感激。
拓展浏览:十个问题弄清 JVM&GC(二)
作者:宜信技术学院 谭文涛