每一个java开发同窗无论是平常工做中仍是面试里,都会遇到JDK、JVM和GC的问题。本文会从如下10个问题为切入点,带着你们一块儿全面了解一下JVM的方方面面。java
上一篇文章结尾时咱们谈到,就JVM的设计规范,从使用用途角度JVM的内存大致的分为:线程私有内存区 和 线程共享内存区。linux
线程私有内存区在类加载器编译某个class文件时就肯定了执行时须要的“程序计数器”和“虚拟栈帧”等所需的空间,而且会伴随着当前执行线程的产生而产生,执行线程的消亡而消亡,所以“线程私有内存区”并不须要考虑内存管理和垃圾回收的问题。线程共享内存区在虚拟机启动时建立,被全部线程共享,是Java虚拟机所管理内存中最应该关注的和最大的一块。首先咱们来一块儿看一下“线程共享内存区”的内存模型是什么样的?面试
如图所示,JVM的内存结构分为堆和非堆两大块区域。算法
之因此这样划分,设计者的目的无非就是为了内存管理,也就是咱们说的垃圾回收。那么什么样的对象是垃圾?垃圾回收算法有哪些?目前经常使用的垃圾回收器又有哪些?这篇文章咱们一块儿弄清楚这些问题和知识点。数组
要想进行垃圾回收,得先知道什么样的对象是垃圾。目前确认对象是否为垃圾的算法主要有两种:引用计数法和可达性分析法。安全
对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,若是一个对象没有任何指针对其引用,它就是垃圾。这种方法虽然很简单、高效,可是JVM通常不会选择这个方法,由于这个方法会出现一个弊端:当对象之间相互指向时,两个对象的引用计数器的值都会加1,而因为两个对象时相互指向,因此引用不会失效,这样JVM就没法回收。服务器
二、可达性分析法:针对引用计数算法的弊端,JVM采用了另外一种算法,以一些"GC Roots"的对象做为起始点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的,便可以进行垃圾回收。不然,证实这个对象有用,不是垃圾。多线程
上图中的obj7和obj8虽然它们互相引用,但从GC Roots出发这两个对象不可达,因此会被标记为垃圾。JVM会把如下几类对象做为GC Roots:并发
注:在可达性分析算法中不可达的对象,并非直接被回收,这时它们处于缓刑状态,至少须要进行两次标记才会肯定该对象是否被回收:框架
第一次标记:若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记;
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法(该方法可将此对象与GC Roots创建联系)。在finalize()方法中没有从新与引用链创建关联关系的,将被进行第二次标记。
第二次标记成功的对象将真的会被回收,若是对象在finalize()方法中从新与引用链创建了关联关系,那么将会逃离本次回收,继续存活。
知道了如何JVM肯定哪些对象是垃圾后,下面咱们来看一下,面对这些垃圾对象,JVM的回收算法都有哪些。
第一步“标记”,以下图所示把堆里全部的对象都扫描一遍,找出哪些是垃圾须要回收的对象,而且把它们标记出来。
(1) 标记和清除两个过程都比较耗时,效率不高
(2) 清除后会产生大量不连续的内存碎片空间,碎片空间太多可能会致使当程序后续须要建立较大对象时,没法找到足够连续的内存空间而不得再也不次触发垃圾回收。
将内存划分为两块区域,每次使用其中一块,当其中一块用满,触发垃圾回收的时候,将存活的对象复制到另外一块上去,而后把以前使用的那一块进行格式化,一次性清除干净。
(清除前)
(清除后)
“标记-复制”算法的缺点显而易见,就是内存空间利用率低。
标记整理算法标记过程仍然与"标记-清除"算法同样,可是后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
将全部存活的对象向一边移动,清理掉存活边界之外的所有内存空间。
结合这三种算法咱们能够看到,
“标记-复制”算法的优势是回收效率高,但空间利用率上有必定的浪费。
而“标记-整理”算法因为须要向一侧移动等一系列操做,其效率相对低一些,但对内存空间管理上十分优异。
所以,“标记-复制”算法适用于那些生命周期短、回收频率高的内存对象,
所以JVM的设计者将JVM的堆内存,分为了两大块区域Young区和Old区,Young区存储的就是那些生命周期短,使用一两次就再也不使用的对象,回收一次基本上该区域十之有八的对象所有被回收清理掉,所以Young区采用的垃圾回收算法也就是“标记-复制”算法。Old区存储的是那些生命周期长,通过屡次回收后仍然存活的对象,就把它们放到Old区中,平时再也不去判断这些对象的可达性,直到Old区不够用为止,再进行一次统一的回收,释放出足够的连续的内存空间。
鉴于Young区和Old区须要采用不一样的垃圾回收算法,所以在JVM的整个垃圾收集器的演进各个时代里,针对Young区和Old区每一个时代都是不一样的垃圾收集机制。从JDK1.3开始到目前,JVM垃圾收集器的演进大致分为四个时代:串行时代、并行时代、并发时代和G1时代。
JDK3(1.3)的时候,大概是2000年左右,那个时代基本计算机都是单核一个CPU的,所以垃圾回收最初的设计实现也是基于单核单线程工做的。而且垃圾回收线程的执行相对于正常业务线程执行来讲仍是STW(stop the world)的,使用一个CPU或者一条收集线程去完成垃圾收集工做,这个线程执行的时候其它线程须要中止。
串行收集器采用单线程stop-the-world的方式进行收集。当内存不足时,串行GC设置停顿标识,待全部线程都进入安全点(Safepoint)时,应用线程暂停,串行GC开始工做,采用单线程方式回收空间并整理内存。单线程也意味着复杂度更低、占用内存更少,但同时也意味着不能有效利用多核优点。所以,串行收集器特别适合堆内存不高、单核甚至双核CPU的场合。
并行收集器是以关注吞吐量为目标的垃圾收集器,也是server模式下的默认收集器配置,对吞吐量的关注主要体如今年轻代Parallel Scavenge收集器上。
并行收集器与串行收集器工做模式类似,都是stop-the-world方式,只是暂停时并行地进行垃圾收集。年轻代采用复制算法,老年代采用标记-整理,在回收的同时还会对内存进行压缩。关注吞吐量主要指年轻代的Parallel Scavenge收集器,经过两个目标参数-XX:MaxGCPauseMills和-XX:GCTimeRatio,调整新生代空间大小,来下降GC触发的频率。并行收集器适合对吞吐量要求远远高于延迟要求的场景,而且在知足最差延时的状况下,并行收集器将提供最佳的吞吐量。
并发标记清除(CMS)是以关注延迟为目标、十分优秀的垃圾回收算法,CMS是针对Old区的垃圾回收实现。
老年代CMS每一个收集周期都要经历:初始标记、并发标记、从新标记、并发清除。其中,初始标记以STW的方式标记全部的根对象;并发标记则同应用线程一块儿并行,标记出根对象的可达路径;在进行垃圾回收前,CMS再以一个STW进行从新标记,标记那些由mutator线程(指引发数据变化的线程,即应用线程)修改而可能错过的可达对象;最后获得的不可达对象将在并发清除阶段进行回收。值得注意的是,初始标记和从新标记都已优化为多线程执行。CMS很是适合堆内存大、CPU核数多的服务器端应用,也是G1出现以前大型应用的首选收集器。
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的垃圾回收工做步骤分为:初始标记、并发标记、最终标记和筛选回收。
这篇文章简单提一下这个最新问世的垃圾收集器,之因此叫“Zero GC”是由于它追求的是更低的GC停顿时间,追求的目标是:支持TB级堆内存(最大4T)、最大GC停顿10ms。JDK11新引入的ZGC收集器,无论是物理上仍是逻辑上,ZGC中已经不存在新老年代的概念了会分为一个个page,当进行GC操做时会对page进行压缩,所以没有碎片问题。因为其是JDK11和只能在64位的linux上使用,所以目前用得还比较少。
以上整体两篇文章七千字,就是我从JVM的做用、设计框架到JVM内存管理的总体的体系化理解。感谢。
拓展阅读:十个问题弄清JVM&GC(二)
做者:宜信技术学院 谭文涛