首页 > 其他 > 详细

图解JVM

时间:2021-04-01 13:51:12      阅读:43      评论:0      收藏:0      [点我收藏+]

JVM

  • 技术分享图片
  • 虚拟机栈:描述的是方法执行时的内存模型 ,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢(下文会看到),主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行 GC
  • 本地方法栈:与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不需要进行 GC
  • 程序计数器:线程独有的, 可以把它看作是当前线程执行的字节码的行号指示器,比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容
  • 本地内存:线程共享区域,Java 8 中,本地内存,也是我们通常说的堆外内存,包含元空间和直接内存,注意到上图中 Java 8 和 Java 8 之前的 JVM 内存区域的区别了吗,在 Java 8 之前有个永久代的概念,实际上指的是 HotSpot 虚拟机上的永久代,它用永久代实现了 JVM 规范定义的方法区功能,主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分由于是在堆中实现的,受 GC 的管理,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM,有人说可以把永久代设置得足够大,但很难确定一个合适的大小,受类数量,常量数量的多少影响很大。所以在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也因此提升了性能(发生 GC 会发生 Stop The Word,造成性能受到一定影响,后文会提到),也就不存在由于永久代限制大小而导致的 OOM 异常了(假设总内存1G,JVM 被分配内存 100M, 理论上元空间可以分配 2G-100M = 1.9G,空间大小足够),也方便在元空间中统一管理。综上所述,在 Java 8 以后这一区域也不需要进行 GC
  • 堆:前面几块数据区域都不进行 GC,那只剩下堆了,是的,这里是 GC 发生的区域!对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收,这块也是我们之后重点需要分析的区域
  • 栈的清除:栈顶指针依次向栈底移动,当前栈顶的数据就认为不在栈里了。

GC

  • 识别垃圾

    • 引用计数法:

      • 对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收
    • 可达性算法

      • 现代虚拟机基本都是采用这种算法来判断对象是否存活,可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。。。(这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。

      • Object的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!

        注意: finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!

        那么这些 GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢,有以下几类

        • 虚拟机栈(栈帧中的本地变量表)中引用的对象

          // a 是栈帧中的本地变量,当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连接,所以对象会被回收。
          	Test a = new Test();
          	a = null;
          
        • 方法区中类静态属性引用的对象:如:static Test s;

        • 方法区中常量引用的对象:static final Test s = new Test();

        • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

垃圾回收算法

垃圾回收主要方法:标记清除算法、复制算法、标记整理算法

  • 标记清除算法

    • 先根据可达性算法标记出相应的可回收对象(图中黄色部分)
    • 对可回收的对象进行回收,会有内存碎片问题
  • 复制算法

    • 把堆等分成两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来(下图有误无需清除),然后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象全部清理掉释放出空间,这样就解决了内存碎片的问题了。
    • 不过复制算法的缺点很明显,比如给堆分配了 500M 内存,结果只有 250M 可用,空间平白无故减少了一半!这肯定是不能接受的!另外每次回收也要把存活对象移动到另一半,效率低下(我们可以想想删除数组元素再把非删除的元素往一端移,效率显然堪忧)
    • 技术分享图片
  • 标记整理法

    • 前面两步和标记清除法一样,不同的是它在标记清除法的基础上添加了一个整理的过程 ,即将所有的存活对象都往一端移动,紧邻排列(如图示),再清理掉另一端的所有区域,这样的话就解决了内存碎片的问题。
    • 但是缺点也很明显:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。
    • 技术分享图片
  • 分代收集算法

    • 分代收集算法整合了以上算法,综合了这些算法的优点,最大程度避免了它们的缺点,所以是现代虚拟机采用的首选算法,与其说它是算法,倒不是说它是一种策略,因为它是把上述几种算法整合在了一起
    • 大部分的对象都很短命,都在很短的时间内都被回收了(IBM 专业研究表明,一般来说,98% 的对象都是朝生夕死的,经过一次 Minor GC 后就会被回收),所以分代收集算法根据对象存活周期的不同将堆分成新生代和老生代(Java8以前还有个永久代),默认比例为 1 : 2,新生代又分为 Eden 区from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1 : 1,这样就可以根据新老生代的特点选择最合适的垃圾回收算法,我们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)。
    • 技术分享图片
  • 分代收集算法工作原理

    • 新生代Minor GC

      • 大部分对象在短时间内都会被回收, 所以经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区同时对象年龄加一(对象的年龄即发生 Minor GC 的次数),最后把 Eden 区对象全部清理以释放出空间
      • 当触发下一次 Minor GC 时,会把 Eden 区的存活对象和 S0(或S1) 中的存活对象(S0 或 S1 中的存活对象经过每次 Minor GC 都可能被回收)一起移到 S1(Eden 和 S0 的存活对象年龄+1), 同时清空 Eden 和 S0 的空间。
      • 若再触发下一次 Minor GC,则重复上一步,只不过此时变成了 从 Eden,S1 区将存活对象复制到 S0 区,每次垃圾回收, S0, S1 角色互换,都是从 Eden ,S0(或S1) 将存活对象移动到 S1(或S0)。也就是说在 Eden 区的垃圾回收我们采用的是复制算法,因为在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象(这也是为啥 Eden:S0:S1 默认为 8:1:1 的原因),S0,S1 区域也比较小,所以最大限度地降低了复制算法造成的对象频繁拷贝带来的开销。
    • 老年代

      • 当对象的年龄达到了我们设定的阈值。默认15,则会从S0(或S1)晋升到老年代
      • 大对象:当某个对象分配需要大量的连续内存时,此时对象的创建不会分配在 Eden 区,会直接分配在老年代,因为如果把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,所以干脆就直接移到老年代.
      • 还有一种情况也会让对象晋升到老年代,即在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。
    • 空间分配担保

      • 在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。
    • Stop The World

      • 如果老年代满了,会触发 Full GC, Full GC 会同时回收新生代和老年代(即对整个堆进行GC),它会导致 Stop The World(简称 STW),造成挺大的性能开销。

        什么是 STW ?所谓的 STW, 即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。

      • Minor GC 用的是复制算法,而在老生代由于对象比较多,占用的空间较大,使用复制算法会有较大开销(复制算法在对象存活率较高时要进行多次复制操作,同时浪费一半空间)所以根据老生代特点,在老年代进行的 GC 一般采用的是标记整理法来进行回收。

垃圾回收器

  • 图片中的垃圾收集器如果存在连线,则代表它们之间可以配合使用

技术分享图片

  • 在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge

  • 在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old

  • 同时在新老生代工作的垃圾回收器:G1

  • Serial

    • Serial 收集器是工作在新生代的,单线程的垃圾收集器,单线程意味着它只会使用一个 CPU 或一个收集线程来完成垃圾回收,不仅如此,还记得我们上文提到的 STW 了吗,它在进行垃圾收集时,其他用户线程会暂停,直到垃圾收集结束,也就是说在 GC 期间,此时的应用不可用。对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器
  • ParNew 收集器

    • ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,其他像收集算法, STW,对象分配规则,回收策略与 Serial 收集器完成一样,在底层上,这两种收集器也共用了相当多的代码

    • ParNew 主要工作在 Server 模式,我们知道服务端如果接收的请求多了,响应时间就很重要了,多线程可以让垃圾回收得更快,也就是减少了 STW 时间,能提升响应时间,所以是许多运行在 Server 模式下的虚拟机的首选新生代收集器,

    • 另一个与性能无关的原因是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作,CMS 是一个划时代的垃圾收集器,是真正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工作,它采用的是传统的 GC 收集器代码框架,与 Serial,ParNew 共用一套代码框架,所以能与这两者一起配合工作,而后文提到的 Parallel Scavenge 与 G1 收集器没有使用传统的 GC 收集器代码框架,而是另起炉灶独立实现的,另外一些收集器则只是共用了部分的框架代码,所以无法与 CMS 收集器一起配合工作。

      在多 CPU 的情况下,由于 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减少 STW 的时间,提升应用的响应速度。

  • Parallel Scavenge 收集器

    • Parallel Scavenge 收集器也是一个使用复制算法多线程,工作于新生代的垃圾收集器

      关注点不同,CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,

      而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。

      Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集时间的 -XX:MaxGCPauseMillis 参数及直接设置吞吐量大小的 -XX:GCTimeRatio(默认99%)

      除了以上两个参数,还可以用 Parallel Scavenge 收集器提供的第三个参数 -XX:UseAdaptiveSizePolicy,开启这个参数后,就不需要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只需要设置好基本的堆大小(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,虚拟机就会根据当前系统运行情况收集监控信息,动态调整这些参数以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标。自适应策略也是 Parallel Scavenge 与 ParNew 的重要区别!

  • Serial Old 收集器

    • Serial 收集器是工作于新生代的单线程收集器,与之相对地,Serial Old 是工作于老年代的单线程收集器,此收集器的主要意义在于给 Client 模式下的虚拟机使用,如果在 Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用,另一种是作为 CMS 收集器的后备预案, 在并发收集发生 Concurrent Mode Failure 时使用
  • Parallel Old 收集器

    • Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,两者组合示意图如下,这两者的组合由于都是多线程收集器,真正实现了「吞吐量优先」的目标
  • CMS

    • CMS 收集器是以实现最短 STW 时间为目标的收集器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是个很不错的选择!

      我们之前说老年代主要用标记整理法,而 CMS 虽然工作于老年代,但采用的是标记清除法,主要有以下四个步骤

      初始标记、并发标记、重新标记、并发清除

    • 技术分享图片

    • 从图中可以的看到初始标记重新标记两个阶段会发生 STW,造成用户线程挂起,不过初始标记仅标记 GC Roots 能关联的对象,速度很快,并发标记是进行 GC Roots Tracing 的过程,重新标记是为了修正并发标记期间因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这一阶段停顿时间一般比初始标记阶段稍长,但远比并发标记时间短

      整个过程中耗时最长的是并发标记和标记清理,不过这两个阶段用户线程都可工作,所以不影响应用的正常使用,所以总体上看,可以认为 CMS 收集器的内存回收过程是与用户线程一起并发执行的。

      但是 CMS 收集器远达不到完美的程度,主要有以下三个缺点

      • CMS 收集器对 CPU 资源非常敏感 原因也可以理解,比如本来我本来可以有 10 个用户线程处理请求,现在却要分出 3 个作为回收线程,吞吐量下降了30%,CMS 默认启动的回收线程数是 (CPU数量+3)/ 4, 如果 CPU 数量只有一两个,那吞吐量就直接下降 50%,显然是不可接受的
      • CMS 无法处理浮动垃圾(Floating Garbage),可能出现 「Concurrent Mode Failure」而导致另一次 Full GC 的产生,由于在并发清理阶段用户线程还在运行,所以清理的同时新的垃圾也在不断出现,这部分垃圾只能在下一次 GC 时再清理掉(即浮云垃圾),同时在垃圾收集阶段用户线程也要继续运行,就需要预留足够多的空间要确保用户线程正常执行,这就意味着 CMS 收集器不能像其他收集器一样等老年代满了再使用,JDK 1.5 默认当老年代使用了68%空间后就会被激活,当然这个比例可以通过 -XX:CMSInitiatingOccupancyFraction 来设置,但是如果设置地太高很容易导致在 CMS 运行期间预留的内存无法满足程序要求,会导致 Concurrent Mode Failure 失败,这时会启用 Serial Old 收集器来重新进行老年代的收集,而我们知道 Serial Old 收集器是单线程收集器,这样就会导致 STW 更长了。
      • CMS 采用的是标记清除法,上文我们已经提到这种方法会产生大量的内存碎片,这样会给大内存分配带来很大的麻烦,如果无法找到足够大的连续空间来分配对象,将会触发 Full GC,这会影响应用的性能。当然我们可以开启 -XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理会导致 STW,停顿时间会变长,还可以用另一个参数 -XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。
  • G1

    • G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器,主要有以下几个特点

      • 像 CMS 收集器一样,能与应用程序线程并发执行。
      • 整理空闲空间更快。
      • 需要 GC 停顿时间更好预测。
      • 不会像 CMS 那样牺牲大量的吞吐性能。
      • 不需要更大的 Java Heap
    • 与 CMS 相比,它在以下两个方面表现更出色

      1. 运作期间不会产生内存碎片,G1 从整体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。
      2. 在 STW 上建立了可预测的停顿时间模型,用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。
    • 为什么G1能建立可预测的停顿模型呢,主要原因在于 G1 对堆空间的分配与传统的垃圾收集器不一样,传统的内存分配就像我们前文所述,是连续的,分成新生代,老年代,新生代又分 Eden,S0,S1,而 G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的 Region,每个Region占有一块连续的虚拟内存地址,如图示技术分享图片

    • 除了和传统的新老生代,幸存区的空间区别,Region还多了一个H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动

      传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小(回收所获得的空间大小及回收所需经验值),这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region, 也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。同时由于只收集部分 Region,可就做到了 STW 时间的可控

    • G1 收集器的工作步骤如下:初始标记、并发标记、最终标记、筛选回收。可以看到整体过程与 CMS 收集器非常类似,筛选阶段会根据各个 Region 的回收价值和成本进行排序,根据用户期望的 GC 停顿时间来制定回收计划。技术分享图片

  • 如果是运行在桌面环境处于 Client 模式的,则用 Serial + Serial Old 收集器绰绰有余,如果需要响应时间快,用户体验好的,则用 ParNew + CMS 的搭配模式,即使是号称是「驾驭一切」的 G1,也需要根据吞吐量等要求适当调整相应的 JVM 参数

JVM调优

  • cpu100%:一般都因为死循环

    • top -c 命令找出当前进程的运行列表,按一下 P 可以按照CPU使用率进行排序
    • 使用命令 top -Hp 2609 找出这个进程下面的线程,继续按P排序,找到十进制的PID,转换为十六进制为:b26
    • jstack -l 2609 > ./2609.stack导出进程快照
    • cat 2609.stack |grep ‘b26‘ -C 8用grep查看线程文件,定位死循环的哪个类、哪一行
  • 内存溢出OOM排查,内存溢出是发生在堆中的,超时,不进行服务,服务挂掉,接口不在服务这样的异常问题会导致内存溢出

    • 1、通过命令查看对应的进程号 比如:jps 或者 ps -ef | grep servicemix
      2、输入命令查看gc情况 命令:jstat -gcutil 进程号 刷新的毫秒数 展示的记录数
      比如:jstat -gcutil 14050 1000 10 (查看进程号14050,每隔1秒获取下,展示10条记录)
      3、查看具体占用情况:
      (1)命令: jmap -histo 进程号 | more (默认展示到控制台)
      (2)命令: jmap -histo 14050 | more > exceptionlog.txt (输出到当前目录的exceptionlog.txt文件)
      比如:jmap -histo 14050 | more 查看具体的classname,是否有开发人员的类,也可以输出到具体文件分析
  • 堆内存调优

    • 引用数据类型是存放在里, 而值数据类型存放在堆栈(栈的结构)里,java8之后的元空间也放在了堆栈,堆栈可以认为是在JVM之外的存储空间
    • 元空间存在于本机物理内存,只受本地内存限制,字符串常量池也在元空间减少了OOM情况,默认最大JVM Heap堆内存-Xmx只用物理内存的1/4最小堆内存-Xms为物理内存的1/64
    • Runtime.getRuntime().totalMemory; // 获取最大堆内存, Runtime.getRuntime()为Runtime Data Area对象

    技术分享图片

    JVM调参(即设置堆内存):
    • 可通过工具进行JVM内存分析:可视化工具:JDK自带的监控程序 jvisualvm (命令行执行此命令) 或 jconsole 或通过 jmap -heap PID 查看
    • -Xms(最小内存)应该等于-Xmx(最大内存)避免堆扩容造成的线上环境堆抖动,也就是避免GC和应用程序争抢内存而导致JVM理论值忽高忽低的问题

GC 调优策略

策略 1:将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。

策略 2:大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。-XX:PretenureSizeThreshold 可以设置直接进入老年代的对象大小。

策略 3:合理设置进入老年代对象的年龄,-XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。

策略 4:设置稳定的堆大小,堆大小设置有两个参数:-Xms 初始化堆大小,-Xmx 最大堆大小。

策略5:注意: 如果满足下面的指标,则一般不需要进行 GC 优化:

MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。

图解JVM

原文:https://www.cnblogs.com/JayV/p/14605667.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!