JVM

130 阅读15分钟

1.JVM执行程序的过程

I.加载class文件 II.管理并分配内存 III.执行垃圾收集

常用性能监控 当应用大量使用内存时,容易造成内存溢出错误,甚至程序崩溃,这种情况下,可以使用软引用来避免OutOfMemoryError,以实现自我保护的目的。

image.png G1和ZGC在中间代表老年代和年轻代混合回收

2.内存分区

垃圾收集只需要关注堆和方法区,垃圾收集回报率高的是堆中内存的回收.常见的垃圾回收策略分为两种:一种是直接回收,即引用计数;另一种是间接回收,即追踪式回收(可达性分析)。引用计数有个致命的缺陷-循环引用,所以 Java 用了可达性分析 image.png

  • 线程共有:

1.堆:

  • 可达性分析其实就是利用标记-清除(mark-sweep),就是标记可达对象,清除不可达对象。 JVM 还需要判断栈上的数据是什么类型,这里又可以分为保守式 GC、半保守式 GC、和准确式 GC。

2.方法区: 是各个线程共享的内存区域,它用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有一个别名NonHeap(非堆),目的应该是与java堆区分开来。方法区的大小决定了系统可以保存多少个类。

  • 线程私有: 1.程序计数器: 由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。 因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器值则为空。 此内存区域是唯一一个在java虚拟机规范中没有规定任何outofmemoryerror的区域.

2.虚拟机栈: 每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

3.本地方法栈 本地方法栈与虚拟机栈所发挥的作用是类似的,其区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务, 而本地方法栈则是为虚拟机使用到的native方法服务。

3.Java对象的回收方式,回收算法

自动内存管理最终可以归结为自动化解决了两个问题:给对象分配内存以及回收分配给对象的内存。

1.对象优先在Eden分配 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发生一次GC.

2.大对象直接进入老生代 所谓的大对象是指,需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。

3.长期存活的对象将进入老生代 既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应放在老生代中。 为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden区出生并经过第一次Minor GC后仍然存活,并且能被Suvivor容纳的话, 将被移动到Suvivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁), 就将会被晋升到老生代中。对象晋升老生代的年龄阈值可以设定。

4.动态对象年龄判定 为了能够更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了设置的阈值才能晋升老生代, 如果在Suvivor空间中相同年龄所有对象大小的总和大于Suvivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老生代, 无须等待阈值要求的年龄。

5.空间分配担保 在学习JVM垃圾收集机制的时候,我们就知道了新生代采用复制算法,但是会造成空间的浪费,故而提出了一种“空间担保机制”来提高复制算法的空间利用率, 所以老年代如果不想产生空间内存碎片那么只能使用“标记-整理”算法了。看到这,我们其实心里肯定有疑问——如何保证老年代有足够的空间来执行空间担保机制呢?

Full GC,是否触发根据经验值判断,即使不允许担保失败,也有可能发生担保失败。 当发生YGC的时候,JVM都会检测之前每次晋升到老年代的对象的平均大小是否大于老年代的剩余内存空间,如果大于,则触发Full GC; 如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,则不会触发Full GC,反之,触发Full GC,保证老年代有足够的空间支持空间分配担保成功。 其实在每次GC发生的时候,我们也不知道到底会有多少对象被回收,又有多少对象能存活。 故而只好取之前每次回收晋升到老年代的对象的平均值作为经验值来判断,但是如果某次GC后存活对象激增,任然会导致担保失败,那么只能重新进行Full GC了, 虽然这样会绕个圈子,但是大部分情况下还是会将HandlePromotionFailure的值设为true,从而 避免Full GC过于频繁。换句话说,就是大部分情况,允许担保失败。

4.垃圾收集器与内存分配策略

一.怎么判断哪些内存是垃圾? 1.引用计数法 2.根搜索算法,java采用的

二.什么时候回收?一个应用程序中那个线程运行有线程自身的优先级来决定的。垃圾回收线程几乎是所有线程中优先级最低的。

三.如何回收?-垃圾回收算法

1.标记-清理算法

2.复制算法

当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列, 与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。 此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间, 也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

3.标记-整理算法

标记/整理算法与标记/清除算法非常相似,它也是分为两个阶段:标记和整理。下面LZ给各位介绍一下这两个阶段都做了什么。   标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。   整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

4.分代收集算法

当前的商业虚拟机的垃圾收集都采用“分代收集”算法。这种算法并没有什么新的意思,只是根据对象的存活周期的不同将内存划分为几块。 一般是把java堆分为新生代和老生代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾回收时都发现有大批对象死去, 只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老生代中因为对象存活率高、没有额外空间对它进行分配担保, 就必须使用“标记-清理”或“标记-整理”算法来进行回收。

5.虚拟机类加载机制

加载 1.bootstrap classloader -引导(也称为原始)类加载器,它负责加载Java的核心类。 2.extension classloader -扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。 在这个实例上调用方法getParent()总是返回空值null,因为引导加载器bootstrap classloader不是一个真正的ClassLoader实例。 3.system classloader -系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器 。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。

6.java的四种引用

强引用、软引用、弱引用、虚引用

  • 强引用(Strong Reference):对象是强引用的时候,即使jvm内存空间不足,GC也不会回收该对象,当满时,报OutOfMemoryError异常
  • 软引用(Soft Reference):JVM内存不足时,会回收软引用,其引用可以关联一个引用队列(需要在一个对象的可达性(是否已被GC回收)发生变化时得到通知,引用队列就是用于收集这些信息的队列)
  • 弱引用(weak Reference):只被弱引用所指向的对象的生命周期是两次GC之间,而只被软引用所指向的对象可以经历多次GC,直到出现内存紧张的情况才会被回收,如weakHashMap
  • 幽灵引用(Phantom Reference):又叫虚引用,创建虚引用则必须指定一个引用队列,当GC准备回收一个对象时如果发现还有虚引用,则会在回收对象的内存之前,把虚引用加入到关联的引用队列中.之后码农可做一个跟踪,用于比较精细的内存使用控制

7.CMS和G1的对比分析

1、CMS收集器(老年代收集器)

1.CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

基于“标记-清除”算法实现,它的运作过程如下:1)初始标记  2)并发标记 3)重新标记  4)并发清除

初始标记、重新标记这两个步骤仍然需要“stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,熟读很快, 并发标记阶段就是进行GC Roots Tracing,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。 优点:并发收集、低停顿。(两次停顿)

缺点: 1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。 2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。 3)CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

常用的配置

image.png

2、G1收集器

G1(Garbage-First))是一款面向服务端应用的垃圾收集器。G1具备如下特点:

  1. 能充分利用多CPU、多核环境下的硬件优势
  2. 可以设置JVM停顿的时间
  3. 新生代老年代混合回收
  4. 将JVM堆内存划分为多个 Region

1、并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。 部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。

3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内

5、G1运作步骤: 1、初始标记;2、并发标记;3、最终标记;4、筛选回收

上面几个步骤的运作过程和CMS有很多相似之处。 初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短, 并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。 最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。

3、ZGC

zgc细节

8.相关工具使用

jprofile使用

查看docker容器内jar 运行信息 jstack 1.进入容器内/bin/bash docker exec 976 -it /bin/bash 2.进入/usr/bin 3.查看容器内pid docker container top redis 4.jstack -l pid

9. GraalVM

10.参数配置

JVM参数配置调优

11.常见问题

image.png

GC 分为两大类,分别是 Partial GC 和 Full GC;

Partial GC 即部分收集,分为 young gc、old gc、mixed gc。

  • young gc:指的是单单收集年轻代的 GC。年轻代的 eden 快要被占满的时候会触发 young gc
  • old gc:指的是单单收集老年代的 GC。
  • mixed gc:这个是 G1 收集器特有的,指的是收集整个年轻代和部分老年代的 GC。

Full GC 即整堆回收,指的是收取整个堆,包括年轻代、老年代,如果有永久代的话还包括永久代。触发条件如下

  • 在要进行 young gc 的时候,根据之前统计数据发现年轻代平均晋升大小比现在老年代剩余空间要大,那就会触发 full gc。
  • 有永久代的话如果永久代满了也会触发 full gc。
  • 老年代空间不足,大对象直接在老年代申请分配,如果此时老年代空间不足则会触发 full gc。
  • 担保失败即 promotion failure,新生代的 to 区放不下从 eden 和 from 拷贝过来对象,或者新生代对象 gc 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 full gc。
  • 执行 System.gc()、jmap -dump 等命令会触发 full gc。

12.相关资料

《深入理解java虚拟机》 《自己动手写Java虚拟机》

JVM合集

JVM调优专栏

JVM简介

blog.csdn.net/a910626/art…

各种jvm总结

OOM异常排查 GC停顿时间过长