敖丙思维导图-JVM知识整理

敖丙思维导图系列目录

这些知识整理都是自己查阅帅丙资料(当然还有其他渠道)加以总结滴~ 每周都会更新知识进去。
如有不全或错误还请大家在评论中指出~


  1. 敖丙思维导图-集合
  2. 敖丙思维导图-多线程之synchronized\ThreadLocal\Lock\Volatitle\线程池
  3. 敖丙思维导图-JVM知识整理
  4. 敖丙思维导图-Spring
  5. 敖丙思维导图-Redis
  6. 敖丙思维导图-RocketMQ+Zookeeper
  7. 敖丙思维导图-Mysql数据库

本文章目录

  • 敖丙思维导图系列目录
    • JVM内存模型
      • 1. 程序计数器(为了线程切换可以恢复到正确执行位置)
      • 2. 虚拟机栈
      • 3. 本地方法栈
      • 4. 堆
      • 5. 方法区(规范)
    • JVM参数分类
    • 类加载机制
      • 类加载器分类
      • 双亲委派模型
        • 双亲委派模型破坏举例
    • 垃圾回收机制
      • 垃圾分代收集
        • 对象何时晋升老年代
      • Stop-The-World机制
      • 可达性分析算法 - 判断对象是否可被回收
      • 哪些对象可以作为 GC Roots
    • 垃圾回收器
      • CMS垃圾回收器(老年代) 并发+标记清除算法 - 最小的停顿时间
      • G1垃圾回收器(同时在新老生代工作)- 可预测垃圾回收的停顿时间
        • 初识G1
        • GC模式
        • 全局并发标记
        • 标记-复制算法停顿时间瓶颈
      • JDK11带来的全新的 ZGC
        • 全并发的ZGC
        • ZGC关键技术(着色指针+读屏障技术)
        • ZGC 调优实践
        • 升级ZGC效果
    • GC性能调优(减少GC+STW)
      • FullGC内存泄露排查
    • 线上问题
      • 排查步骤
    • 其它问题
      • java中的内存泄漏
      • 虚拟机如何判定两个Java类相同


敖丙思维导图-JVM知识整理_第1张图片

JVM内存模型

以Sun HotSpot虚拟机为例。Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

其中方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的(编译时确定所需内存大小)。

敖丙思维导图-JVM知识整理_第2张图片

1. 程序计数器(为了线程切换可以恢复到正确执行位置)

如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域,不需要进行 GC

2. 虚拟机栈

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

  • 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError
  • 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

3. 本地方法栈

虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。
在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。会抛出StackOverflowError+OutOfMemory

4. 堆

堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。

  • 能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

为了保证对象的内存分配过程中的线程安全性,HotSpot虚拟机提供了一种叫做TLAB(Thread Local Allocation Buffer)的技术。堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分,他在读取上确实是线程共享的,但是在内存分配上,是线程独享的。

5. 方法区(规范)

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。在JDK1.8中,使用元空间代替永久代来实现方法区(元空间并不在虚拟机中,而是使用本地内存),元数据空间并不在虚拟机中,而是使用本地内存(不需要进行 GC)。但会出现OutOfMemoryError

JVM参数分类

  1. 标准参数,即在JVM的各个版本中基本不变的,相对比较稳定的参数
-help
-version -showversion
  1. X参数,非标准化参数,变化比较小的参数
-Xint:解释执行
-Xcomp:第一次使用就编译成本地代码
-Xmixed:混合模式,JVM自己来决定是否编译成本地代码,默认使用的就是混合模式
  1. XX参数,特点是非标准化参数,相对不稳定,主要用于JVM调优和Debug
1、Boolean类型
格式:-XX[+-]<name>其中+-表示启用或者禁用name属性
比如:-XX:+UseConcMarkSweepGC表示启用CMS垃圾收集器,-XX:+UseG1GC表示启用G1垃圾收集器
2、非Boolean类型
格式:-XX:<name>=<value>表示name属性的值是value
比如:-XX:MaxGCPauseMillis=500表示GC的最大停顿时间是500毫秒,-XX:GCTimeRatio=19

注意:-Xmx和-Xms表示设置JVM的最大内存和最小内存,它们不是X参数,而是XX参数,-Xmx等价于-XX:MaxHeapSize,-Xms等价于-XX:InitialHeapSize;-Xss设置堆栈,也是XX参数,等价于-XX:ThreadStackSize

类加载机制

包括以下 7 个阶段:

  • 加载(Loading)
    通过类的完全限定名称获取定义该类的二进制字节流。
    将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
    在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口
  • 验证(Verification)
    Class 文件的字节流中包含的信息符合当前虚拟机的要求
  • 准备(Preparation)
    准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。

初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
但是加上final后就会初始化为123

  • 解析(Resolution)
    将常量池的符号引用替换为直接引用的过程。
  • 初始化(Initialization)
    执行类构造器。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,通过程序制定的主观计划去初始化类变量和其它资源。
  • 使用(Using)
  • 卸载(Unloading)

类加载器分类

  • 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换

双亲委派模型破坏举例

原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现。在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,JDBC通过线程上下文件类加载器Thread.currentThread().getContextClassLoader()得到线程上下文加载器来加载Driver实现类。)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。

class com.mysql.jdbc.Driver-----sun.misc.Launcher$AppClassLoader@18b4aac2
class com.mysql.fabric.jdbc.FabricMySQLDriver-----sun.misc.Launcher$AppClassLoader@18b4aac2
DriverManager classLoader:null

在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是JAVA核心库提供的,而JAVA核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),JAVA的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以设置的上下文类加载器来实现对于接口实现类的加载

垃圾回收机制

  1. 标记 - 清除(老年代CMS用这个回收)
    在标记阶段,程序会检查每个对象是否为活动对象,是活动对象,则在对象头部打上标记。
    在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
    在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
  • 标记和清除过程效率都不高;
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
  1. 标记 - 整理
    让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 优点是不会产生内存碎片
  • 需要移动大量对象,处理效率比较低。
  1. 复制(新生代)
    将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
    主要不足是只使用了内存的一半。
    现在的商业虚拟机划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
    HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

垃圾分代收集

  • 简化了新对象的分配(仅仅在新生代分配内存);
  • 能够更有效的清除不再须要的对象(即死对象,新生代和老年代使用不同的回收算法)。
  1. 新生代对象一般分配在 Eden 区,当 Eden 区将满时,触发 Minor GC。
  2. 大部分对象在短时间内都会被回收, 所以经过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(Eden: S0: S1 = 8:1:1),同时对象年龄加一,把 Eden 区对象全部清理。
  3. 触发下一次 Minor GC,把 Eden 区的存活对象和 S0中的存活对象移动到S1, 同时清空 Eden 和 S0 。
  • 新生代使用复制算法,因为在 Eden 区对象大部分在 Minor GC 后都消亡,S0,S1 区域也比较小,降低了复制算法造成的对象频繁拷贝带来的开销

对象何时晋升老年代

  • 当对象的年龄达到了我们设定的阈值
  • 大对象
  • S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代

JVM优化,就是尽可能的让对象都在年轻代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免年轻代频繁的进行垃圾回收。

敖丙思维导图-JVM知识整理_第3张图片

Stop-The-World机制

Java中Stop-The-World机制简称STW,在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。

可达性分析算法 - 判断对象是否可被回收

以 GC Roots 为起始点进行搜索,引出它们指向的下一个节点。。。直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。
对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法。 finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收

哪些对象可以作为 GC Roots

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象 (Test a = new Test();)
  • 本地方法栈中 JNI(Native) 中引用的对象
  • 方法区中类静态属性引用的对象 (public static Test s;)
  • 方法区中的常量引用的对象 (public static final Test s = new Test();)

Java 提供了四种强度不同的引用类型。(强引用 软引用 弱引用 虚引用)

垃圾回收器

CMS垃圾回收器(老年代) 并发+标记清除算法 - 最小的停顿时间

CMS是一款并发、使用标记-清除算法、以获取最小停顿时间为目的、对老年代进行回收的GC。CMS虽然是老年代的gc,但仍要扫描新生代。(GC ROOT TRACING可到达)

  • CMS垃圾回收器一般与ParNew多线程收集器(新生代, Serial 收集器的多线程版本)配合工作。

3.1 初始标记(STW) - 可达性分析,标记GC ROOT能直接关联到的对象。
3.2 并发标记 - 由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。(GC Roots Tracing,耗时最长 )+并发预清理 - 标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
3.3 重标记(STW) - 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
3.4 并发清理 - 用户线程被重新激活,同时清理那些无效的对象。+ 重置 - CMS清除内部状态,为下次回收做准备。

  • 并发意味着多线程抢占CPU资源,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。
  • 并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。无法处理浮动垃圾。(不能等到老年代满了再进行GC)
  • 标记-清除算法可能造成大量的空间碎片。空间碎片过多,就会给大对象分配带来麻烦。

G1垃圾回收器(同时在新老生代工作)- 可预测垃圾回收的停顿时间

在G1提出之前,经典的垃圾收集器主要有三种类型:串行收集器、并行收集器和并发标记清除收集器,这三种收集器分别可以是满足Java应用三种不同的需求:内存占用及并发开销最小化、应用吞吐量最大化和应用GC暂停时间最小化,但是,上述三种垃圾收集器都有几个共同的问题:(1)所有针对老年代的操作必须扫描整个老年代空间;(2)新生代和老年代是独立的连续的内存块,必须先决定年轻代和老年代在虚拟地址空间的位置。

初识G1

G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。

  • G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
  • 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
  • G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。
  • G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但是都是2的冥次方。

敖丙思维导图-JVM知识整理_第4张图片

新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。

GC模式

针对新生代和老年代,G1提供2种GC模式,Young GC和Mixed GC,两种会导致Stop The World

  • Young GC 当新生代的空间不足时,G1触发Young GC回收新生代空间 Young GC主要是对Eden区进行GC,它在Eden空间耗尽时触发,基于分代回收思想和复制算法,每次Young GC都会选定所有新生代的Region,同时计算下次Young GC所需的Eden区和Survivor区的空间,动态调整新生代所占Region个数来控制Young GC开销
  • Mixed GC 当老年代空间达到阈值会触发Mixed GC,选定所有新生代里的Region,根据全局并发标记阶段(下面介绍到)统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内,尽可能选择收益高的老年代Region进行GC,通过选择哪些老年代Region和选择多少Region来控制Mixed GC开销

全局并发标记

全局并发标记主要是为Mixed GC计算找出回收收益较高的Region区域
敖丙思维导图-JVM知识整理_第5张图片

4.1 初始标记(STW):暂停所有应用线程(STW),并发地进行标记从 GC Root 开始直接可达的对象(原生栈对象、全局对象、JNI 对象),当达到触发条件时,G1 并不会立即发起并发标记周期,而是等待下一次新生代收集,利用新生代收集的 STW 时间段,完成初始标记,这种方式称为借道(Piggybacking)
4.2 根区域扫描:在初始标记暂停结束后,新生代收集也完成的对象复制到 Survivor 的工作,应用线程开始活跃起来; 此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象,需要找出哪些对象存在对老年代对象的引用,把这些对象标记成根(Root); 扫描的 Suvivor 分区也被称为根分区(Root Region);
4.3 并发标记:标记各个堆中Region的存活对象信息
4.4 重新标记(STW): 和CMS类似暂停所有应用线程(STW),以完成标记过程短暂地停止应用线程, 标记在并发标记阶段发生变化的对象,和所有未被标记的存活对象,同时完成存活数据计算。
4.5 清理(STW):整理更新每个Region各自的RSet;回收不包含存活对象的Region;统计计算回收收益高(基于释放空间和暂停目标)的老年代分区集合

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片
  • STW可预测的停顿:用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。
  • g1 回收后立马合并空闲内存,CMS在stw的时候做

传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小,这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。

STW 过程中,初始标记因为只标记 GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1 停顿时间的瓶颈主要是标记-复制中的转移阶段 STW。
为什么转移阶段不能和标记阶段一样并发执行呢?主要是 G1 未能解决转移过程中准确定位对象地址的问题。

标记-复制算法停顿时间瓶颈

CMS 新生代的 Young GC、G1 和 ZGC 都基于标记-复制算法。标记-复制算法可以分为三个阶段:

  • 标记阶段,即从 GC Roots 集合开始,标记活跃对象;
  • 转移阶段,即把活跃对象复制到新的内存地址上;
  • 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。

JDK11带来的全新的 ZGC

全并发的ZGC

ZGC 也采用标记-复制算法,但ZGC 在标记、转移和重定位阶段几乎都是并发的,这是 ZGC 实现停顿时间小于10ms目标的最关键原因。

  1. 初始化标记,和CMS以及G1一样,主要做Root集合扫描,「GC Root是一组必须活跃的引用,而不是对象」。
  2. 并发标记阶段,这个阶段在第一步的基础上,继续往下标记存活的对象。
  3. 选取接下来需要标记整理的Region集合
  4. 并发回收,这个阶段会把上一阶段选中的需要整理的Region集合中存活的对象移到一个新的Region中. (原来占用的Region就能马上回收并被用于接下来的对象分配。)
  • 它与G1一样,都是基于Region设计的垃圾回收器,ZGC中的Region也被称为「ZPages」。G1的每个Region大小是完全一样的,而ZGC的Region大小分为3类:2MB,32MB,N×2MB。
  • G1和ZGC在回收的时候,它们只会选择一部分Region进行回收,这个回收过程采用的是Mark-Compact算法,即将待回收的Region中存活的对象拷贝到一个全新的Region中,这个新的Region对象分配就会非常紧凑,几乎没有碎片。垃圾回收算法这一点上,和G1是一样的。
  • ZGC是能自动感知NUMA架构并充分利用NUMA架构特性的。

ZGC 只有三个 STW 阶段:初始标记,再标记,初始转移。

其中,初始标记和初始转移分别都只需要扫描所有 GC Roots,其处理时间和 GC Roots 的数量成正比,一般情况耗时非常短;再标记阶段 STW 时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC 几乎所有暂停都只依赖于 GC Roots 集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与 ZGC 对比,G1 的转移阶段完全 STW 的,且停顿时间随存活对象的大小增加而增加。

ZGC关键技术(着色指针+读屏障技术)

ZGC 通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着 GC 线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在 ZGC 中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。

  • Load Barriers读屏障(之前的GC都是采用Write Barrier)尝试读取堆中的一个对象引用obj.fieldA并赋给引用o(fieldA也是一个对象时才会加上读屏障)。如果这时候对象在GC时被移动了,接下来JVM就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针“修正”到原本的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW。
  • JVM是如何判断对象被移动过呢?就是利用上面提到的颜色指针,如果指针是Bad Color,那么程序还不能往下执行,需要「slow path」,修正指针;如果指针是Good Color,那么正常往下执行即可。(有点像CAS自旋)

ZGC 中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。

ZGC 调优实践

ZGC 的核心特点是并发,GC 过程中一直有新的对象产生。如何保证在 GC 完成之前,新产生的对象不会将堆占满,是 ZGC 参数调优的第一大目标。

开启”基于固定时间间隔“的 GC 触发机制
 -XX:ZCollectionInterval:比如调整为5秒,甚至更短。
增大修正系数 
-XX:ZAllocationSpikeTolerance,更早触发 GC。ZGC 采用正态分布模型预测内存分配速率,模型修正系数 ZAllocationSpikeTolerance 默认值为2,值越大,越早的触发 GC。Zeus 中所有集群设置的是5。

升级ZGC效果

  • 延迟降低
    TP(Top Percentile)是一项衡量系统延迟的指标:TP999 表示99.9%请求都能被响应的最小耗时;TP99 表示99%请求都能被响应的最小耗时。
在 Zeus 服务不同集群中,ZGC 在低延迟(TP999 < 200ms)场景中收益较大:
超低延迟(TP999 < 20ms)和高延迟(TP999 > 200ms)服务收益不大,原因是这些服务的响应时间瓶颈不是 GC,而是外部依赖的性能。
  • 吞吐下降
    对吞吐量优先的场景,ZGC 可能并不适合。例如,Zeus 某离线集群原先使用 CMS,升级 ZGC 后,系统吞吐量明显降低。究其原因有二:第一,ZGC 是单代垃圾回收器,而 CMS 是分代垃圾回收器。单代垃圾回收器每次处理的对象更多,更耗费 CPU 资源;第二,ZGC 使用读屏障,读屏障操作需耗费额外的计算资源。

敖丙思维导图-JVM知识整理_第6张图片

GC性能调优(减少GC+STW)

  • 设置堆的最大最小值(Xmx和Xms),为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。
  • 设置老年和年轻代的比例 ,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
  • 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代.
  • 配置好的机器使用并发收集算法。
  • 每个线程默认开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般512K就足用。

可以通过下面的参数打Heap Dump信息。
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
通过下面参数可以控制OutOfMemoryError时打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError

FullGC内存泄露排查

  • 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
  • 内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.
  • 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)
  • 程序BUG导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.
  • 代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。
  • JVM参数设置问题:包括总内存大小、新生代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。

JDK的自带工具,包括jstack(查看线程)jmap(查看内存)jstat(性能分析,堆内存各部分的使用量以及加载类的数量)等常用命令:

#查看堆内存各区域的使用率以及GC情况
jstat -gcutil -h20 pid 1000
#查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20
#dump堆内存文件
jmap -dump:format=b,file=heap pid

dump下来,用可视化的堆内存分析工具:JVisualVM、JConsole、MAT等

线上问题

  • OOM 问题:首先要配置-XX:+HeapDumpOnOutOfMemoryError,dump堆内存查看

创建了大量线程、对象,导致垃圾回收器来不及回收,分配的堆内存被占满,产OutOfMemoryError错误。
产生栈溢出的场景:比如死循环中创建对象。

  • 栈溢出(StackOverflowError):如果你确认递归实现是正确的,为了允许大量的调用,你可以增加栈的大小。依赖于安装的 Java 虚拟机,默认的线程栈大小可能是 512KB 或者 1MB。你可以使用 -Xss 标识来增加线程栈的大小。

Java的内存结构中,栈的大小不是无限的。大量的方法调用过程,导致不断压栈最终将栈内存占满,产生StackOverflowError错误,程序直接终止运行。
产生栈溢出的场景:比如不合理(递归太深)的递归调用。

  • 内存泄漏 (1.尽量降低变量的作用域,以及及时把对象复制为可清理对象(null)2.在各种IO或者数据库连接时,都需要在最后通过close()方法释放对象,这里也是长对象引用短对象时造成的内存泄漏)

当很多对象使用之后已经没有再使用的必要而没有置为null,导致垃圾回收器无法对其回收,造成内存资源的大量浪费,给系统带来很多不稳定因素。
产生内存溢出的场景:比如使用静态的集合。

// 如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
public class Simple   {
     
       Object object;
       void method () {
     
              object = new Object();
        }
}
  • 线程死锁,锁争用
  • Java进程消耗CPU过高
    (1)使用 top -Hp 命令找出进程中占用cpu最高的前几个线程
    (2)使用jstack获取线程快照,然后使用线程id搜索分析快照文件
    (3)如果线程调用了业务相关代码,则分析是否是代码问题导致的cpu占用过高,如果线程是VM Thread,则应该监控检查垃圾回收活动频率,看是否是因为频繁进行垃圾回收导致的。

排查步骤

  1. jmap -heap 233047
    敖丙思维导图-JVM知识整理_第7张图片
  2. jstat -gcutil -h20 233047 1000
    敖丙思维导图-JVM知识整理_第8张图片

其它问题

java中的内存泄漏

内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和CG ROOTS不可达,那么GC也是可以回收它们的。

  1. 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
  2. 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。

虚拟机如何判定两个Java类相同

不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。

你可能感兴趣的:(面试复习)