当前位置:   article > 正文

总结-JVM基础

总结-JVM基础

部分内容可能来自网络或者由GPT生成。
如有雷同,纯属巧合,仅供学习参考之用。

1 JVM运行时数据区域

  • 堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器;
  • Heap(堆):
    • 对象的实例以及数组的内存都是要在堆上进行分配的,堆是线程共享的一块区域,用来存放对象实例,堆是垃圾收集器管理的主要区域,因此堆又被称为 “GC (Garbage Collected) 堆”;如果堆中没有足够的内存完成对象实例的分配且也无法扩展时,抛出OOM异常。
    • 堆细分:新生代、老年代,对于新生代又分为:Eden区和Surviver1和Surviver2区,详细可以看堆内存分配策略。
  • 方法区 (Method Area):
    • 对于JVM的方法区也可以称之为永久区,存储已被JVM加载的类 (Class) 信息,静态常量 (static final),静态变量等信息,当方法去无法满足内存分配时抛出OOM异常。
    • Jdk1.8以后取消了方法区这个概念,称之为元空间(MetaSpace),当应用中的 Java 类过多时,比如 Spring 等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出:
      • 在JDK 1.7之前,运行时常量池放在方法区中,HotSpot对方法区的实现是永久代;
      • 在JDK1.7时,将运行时常量池中的字符串常量池从方法区中移到堆里,剩下的东西还在方法区中;
      • 在JDK1.7之后,字符串常量池还在堆中,运行时常量池还在方法区中,只不过HotSpot 移除了永久代 , 使用 元空间 (Metaspace) 代替永久代,元空间使用的是物理内存,直接受到本机内存的限制。
  • 虚拟机栈 (VM Stack):
    • 虚拟机栈是线程私有的,每个线程拥有自己的虚拟机栈,所以虚拟机栈生命周期与线程一样。虚拟机栈是用来专门描述Java中非Native方法的内存模型。每个方法在执行时就会在当前线程的虚拟机栈中创建一个栈帧(包括局部变量表,操作数栈,动态链接,方法出口等信息)。每一个方法从调用到执行完成,就是虚拟机栈中一个栈帧压栈到弹栈的全部过程,栈帧中用来存放(局部变量表、操作数栈 、动态链接 、返回地址);
      • 局部变量表:局部变量表是一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量,底层是变量槽(variable slot)。
      • 操作数栈:是用来记录一个方法在执行的过程中,字节码指令向操作数栈中进行入栈和出栈的过程。大小在编译的时候已经确定了,当一个方法刚开始执行的时候,操作数栈中是空发的,在方法执行的过程中会有各种字节码指令往操作数栈中入栈和出栈。
      • 动态链接:因为字节码文件中有很多符号的引用,这些符号引用一部分会在类加载的解析阶段或第一次使用的时候转化成直接引用,这种称为静态解析;另一部分会在运行期间转化为直接引用,称为动态链接。
      • 返回地址:类型(指向了一条字节码指令的地址)JIT即时编译器(Just In Time Compiler),简称 JIT 编译器,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,比如锁粗化等。
    • 在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  • 本地方法栈(Native Method Stack)
    • 本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
  • PC程序计数器(Program Counter Register, PCR):
    • PC,指的是存放下一条指令的位置的一个指针。它是一块较小的内存空间,且是线程私有的,可以看作是当前线程所执行的字节码的行号指示器。由于线程的切换,CPU在执行的过程中,需要记住原线程的下一条指令的位置,为了切换线程后CPU还能回到正确的执行位置,每一个线程都应该又一个独立的PCR,互不影响。
    • 若当前执行的是非Native方法,则PCR记录的是当前字节码指令的地址;若是Native方法,则PCR为空(Undefined)。PCR所在的内存区域是运行时数据区里唯一不会发生OOM (OutOfMemoryError) 异常的区域。

2 堆内存分配策略

  • 堆的划分:
    • Java堆分为新生代(Young Generation)和老年代(Old Generation),又将新生代分为Eden区,From Survivor区,To Survivor区,它们默认的内存分配比例为8:1:1。
  • 对象的分配:对象优先在 Eden 区分配、大对象直接进入老年代、长期存活的对象进入老年代、动态对象年龄判定、空间分配担保。
    • 新建的对象一般被优先分配到Eden和From区(需要较大连续空间的对象直接被移入老年代),如果Eden区没有足够的空间进行分配时,虚拟机执行一次MinorGC。
    • 经过一次Minor GC后,Eden和From中存活的对象被移入To区,然后这个To区成为下一次Minor GC的From区,继续扫描(From区和To区时不断交替互相成为),当移入To区时如果To区空间不够用,就会将To区无法容纳的对象放入老年代。
    • 大对象直接进入老年代(需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
    • 长期存活的对象进入老年代,虚拟机为每个对象定义了一个年龄(Age Count)计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值(默认15次),对象进入老年区。(动态对象年龄判定:程序从年龄最小的对象开始累加,如果累加的对象大小,大于幸存区的一半,则将当前的对象 age 作为新的阈值,年龄大于此阈值的对象则直接进入老年代)
    • 每次进行Minor GC或者大对象直接进入老年区时,JVM会计算所需空间大小如小于老年区的剩余值大小,则进行一次Full GC。
  • Minor GC、Major GC、Full GC
    • Minor GC 在年轻代空间不足的时候发生,在新生代中,采用复制算法;
    • Major GC 发生在老年代中,出现 MajorGC 一般经常伴有 MinorGC,采用标记—清除算法或标记—整理算法;
    • Full GC
      • 当老年代无法再分配内存的时候;
      • 元空间不足的时候;
      • 显示调用 System.gc 的时候。
      • 另外,像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 的时候也会发生 FullGC。包括一次Minor GC和一次Major GC。
    • Java对象具有“朝生夕灭”的特点(即生命周期很短),所以Minor GC十分频繁,速度快;而Major GC比Minor GC速度一般慢十倍以上。
  • 判断对象的是否需要回收:
    • 首先判断对象是否可用(有两种算法):引用计数算法,可达性分析算法。
    • 引用计数算法:给对象添加一个引用计数器,被引用一次则+1,当引用失效就-1,当计数器为0时该对象就是不可再被引用。(这种算法实现简单效率也高,但很难处理对象间相互循环引用的情况)。
    • 可达性分析算法:在Java中,通过可达性分析算法来判断对象是否存活。通过一系列称为 GC Roots 的对象作为起点,向下搜索,走过的路径称为引用链 (Reference Chain),当有对象与GC Roots不可达时,该对象不可再被引用。
      • 注:当对象与GC Roots不可达时,也不意味着对象已经死亡了,真正宣布对象死亡还需要两次标记过程:1)第一次标记并筛选,筛选的条件为该对象是否有必要执行 finalize() 方法,若对象没有覆盖 finalize() 方法,或该方法已经被JVM调用过的,则认为没有必要执行 finalize() 方法,回收该对象;2)若该对象有必要执行 finalize() 方法,则该对象会被放入一个叫 F-Queue的队列中,并由JVM创建的 Finalizer 线程执行 finalize() 方法进行回收。

3 对象的创建与定位

  • 对象的创建:类加载检查、分配内存、初始化零值、设置对象头、执行init方法;
    • 类加载检查:虚拟机遇到 new 指令时,⾸先去检查是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
    • 分配内存:在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存,分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
    • 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
    • 设置对象头:初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
    • 执⾏ init ⽅法:从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说(除循环依赖),执⾏ new 指令之后会接着执⾏init⽅法,这样⼀个真正可⽤的对象才算产⽣出来。
  • 对象的定位:
    • Java程序通过栈上的一个引用来定位到堆上的对象,定位的方式有两种:句柄,直接指针;
    • 句柄:堆中会划分出一块句柄池,栈中的某个引用存的是其实是对象的句柄地址,句柄地址中包含了对象的实例地址与对象类类型数据地址。其好处是当对象被频繁移动时,修改的只是句柄中的对象实例地址,而引用本身不变。
    • 直接指针:直接存储对象的实例地址(HotSpot采用这种方式)。其好处是节省了一次指针定位的时间开销,速度快。
  • 对象引用
    • 普通的对象引用关系就是强引用。
    • 软引用用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
    • 弱引用对象相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
    • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。它唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:PhantomReference。

4 JVM类加载过程

  • 类加载过程:加载,链接,初始化。其中链接又分为3个步骤:验证,准备,解析,所以总体过程:加载、验证、准备、解析、初始化。
    • 加载阶段:
      • 通过一个类的全限定名来获取定义此类的二进制字节流。
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
      • 在Java堆中生成一个代表这个类的java.lang.class对象,作为方法区这些数据的访问入口。
    • 验证阶段:
      • 文件格式验证(是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理)
      • 元数据验证(对字节码描述的信息进行语意分析,以保证其描述的信息符合Java语言规范要求)
      • 字节码验证(保证被校验类的方法在运行时不会做出危害虚拟机安全的行为)
      • 符号引用验证(虚拟机将符号引用转化为直接引用时,解析阶段中发生)
    • 准备阶段:
      • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。将对象初始化为“零”值
    • 解析阶段:
      • 解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
    • 初始化
      • 实际上是执行类构造器 () 方法的过程,为静态变量真正赋值的过程。虚拟机保证父类的() 优于子类的执行(这也是父类的静态代码块(static {})优于子类静态代码块执行的原因),所以虚拟机中执行第一个() 的类永远是Object。
      • JVM确实规定了几种会触发类和接口初始化的情景,这些情景被称为“主动引用”。有且只有五种主动引用方式必须对类与接口进行初始化:
        • 创建类的实例:当使用 new 关键字创建类的实例时,会触发该类的初始化。
        • 访问类的静态变量或为静态变量赋值:当首次访问类的静态字段或对静态字段赋值时,会触发该类的初始化。
        • 调用类的静态方法:当首次调用类的静态方法时,会触发该类的初始化。
        • 反射:使用 Java 的反射机制对类进行反射操作时,如果类还没有初始化,则会触发类的初始化。
        • 初始化一个类的子类:初始化一个类的子类会触发父类的初始化。(但不会触发其他接口的初始化,仅对父类有效)
      • 被动引用则不会触发类初始化。例如,通过子类引用静态字段不会触发父类初始化,使用静态常量(编译期常量)不会触发类初始化等。了解这些主动引用方式有助于更好地理解类加载机制和优化程序性能。
  • 类加载器
    • 执行加载阶段的第一步的代码模块,称为“类加载器”。只有在同一类加载器的情况下,比较两个类是否“相等”才有意义,否则即使有两个同名的类(来自同一Class文件),它们也不等。(这里的相等包括equal() 方法,instanceof,isAssignableFrom()方法)。
    • JVM预定义了3种类加载器:启动类加载器 (BootStrap ClassLoader),扩展类加载器 (Extension ClassLoader),系统类加载器(又叫应用程序加载器) (System ClassLoader)。
      • 启动类加载器:用于加载%JAVA_HOME%\lib 下的Java和心累,不允许开发者直接引用。
      • 扩展类加载器:用于加载JRE的扩展类,即JRE目录下的 lib\ext,开发者可直接引用。
      • 系统类加载器:用于加载CLASSPATH下的类,开发者可直接引用。
  • 双亲委派机制
    • 双亲委派机制(Parent Delegation Model)是一种类加载机制,它定义了类加载器在加载一个类时,通过优先将请求委派给父类加载器再逐层加载的工作方式。其主要步骤如下:
      • 当一个类加载器收到类加载请求时,先不会自己去加载,而是把请求委派给父类加载器。
      • 父类加载器也按照相同的方式向上委派,直到委派到最顶层的Bootstrap ClassLoader。
      • 如果父类加载器能够完成类加载任务,就返回成功;否则,子类加载器才会尝试自己加载该类。
    • 双亲委派机制的好处
      • 避免类重复加载:通过委派机制,同一个类在父类加载器中已经被加载,就不会再被子类加载器重新加载,从而避免类重复加载。
      • 保证核心类库的安全性:避免核心Java类库(如 java.lang.Object)被自定义类库或第三方类库覆盖,确保了系统的安全性和稳定性。
      • 类一致性:委派机制保证了核心库和标准库的类一致性,避免不同加载器加载的相同类在 JVM 中出现多份实例。
      • 可扩展性:用户可以自定义类加载器来加载非标准路径或者特定需求的类,但又不会破坏整体的类加载体系。
    • 破坏双亲委派机制的情况
      • 尽管双亲委派机制有诸多好处,但在某些场景下,可能需要打破这种机制:
        • Ant、Tomcat等工具的自定义类加载器:这些工具需要在运行时动态加载类和资源,它们可能需要从不同的路径或者自定义的仓库中加载而不是 JVM 的标准路径。
        • OSGi的模块化加载:在OSGi这样的模块化系统中,需要每个模块独立加载和管理自己的类和资源,有时候需要破坏双亲委派机制来实现独立的模块加载。
        • 热部署/热替换:一些应用服务器支持应用程序的热部署或热替换,即在不重启服务器的情况下更新应用程序。这种场景下,也需要破坏双亲委派机制,以使得类可以在不同的类加载器中被重新加载。
        • Java Agent:某些 Java Agent 可能会修改类加载器的行为,实现功能注入,这也涉及到对双亲委派机制的破坏。
      • 如何破坏双亲委派机制
        • 自定义ClassLoader:重写类加载器的 loadClass 方法,绕过 super.loadClass(name) 调用,直接加载类。
1public class CustomClassLoader extends ClassLoader {
2    
3    @Override
4    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
5        // 自定义类加载逻辑,跳过默认的双亲委派
6        Class<?> clazz = findLoadedClass(name);
7        if (clazz == null) {
8            try {
9                clazz = findClass(name);
10            } catch (ClassNotFoundException e) {
11                // ignore
12            }
13        }
14        return (clazz != null) ? clazz : getParent().loadClass(name);
15    }
16}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  - 子类优先加载(先尝试自己加载再委派给父类):子类加载器先尝试加载类,如果找不到,再委派给父类加载器。这打破了双亲委派机制的默认行为。
  • 1
1@Override
2public Class<?> loadClass(String name) throws ClassNotFoundException {
3    // 自定义逻辑:先尝试自己加载,找不到再委派
4    Class<?> clazz = findLoadedClass(name);
5    if (clazz == null) {
6        try {
7            clazz = findClass(name);
8        } catch (ClassNotFoundException e) {
9            clazz = getParent().loadClass(name);
10        }
11    }
12    return clazz;
13}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 使用不同的类加载器加载不同的模块:创建多个类加载器并分别加载不同的模块或组件,可以实现加载隔离和热替换等功能。
  • tomcat的类加载机制
    • Tomcat 是一个典型的 Java Web 容器,其类加载机制是构建在 Java 标准类加载机制上的扩展,用于处理复杂的 Web 应用程序环境。理解 Tomcat 的类加载机制对部署和排除类加载冲突问题非常有帮助。以下是对 Tomcat 类加载机制的简要介绍:
    • Tomcat 的类加载器层级结构如下图所示:
Bootstrap ClassLoader
             ↑
        System ClassLoader
             ↑
      Common ClassLoader(公共类加载器)
             ↑
        Catalina ClassLoader(Tomcat内部类加载器)
             ↑
        Shared ClassLoader(共享类加载器,可选)
             ↑
        Webapp ClassLoader(Web应用类加载器,每个Web应用独有一个)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 每层类加载器的职责
    • Bootstrap ClassLoader(引导类加载器):负责加载 JVM 核心类库(如 rt.jar)。
    • System ClassLoader(系统类加载器):负责加载类路径(classpath)上指定的类和 JAR 包。
    • Common ClassLoader(公共类加载器):加载 Tomcat 的公共类库,通常位于 $CATALINA_HOME/lib 和 $CATALINA_BASE/lib 目录下。这些类库在所有 Web 应用中共享。
    • Catalina ClassLoader(Tomcat内部类加载器):加载 Tomcat 自身所需的核心类,大多数 Tomcat 核心功能都在这一层加载。
    • Shared ClassLoader(共享类加载器,可选):加载共享类库,通常位于 $CATALINA_HOME/shared/lib 目录下。这些类库在不同的 Web 应用之间共享。
    • Webapp ClassLoader(Web应用类加载器):每个 Web 应用都有一个独立的 Webapp ClassLoader,负责加载应用自身的类和资源,通常位于 WEB-INF/classes 和 WEB-INF/lib 目录下。
  • 举例来说,当一个 Tomcat Web 应用请求加载一个类时,类加载顺序如下:
    • 引导类加载器 先尝试加载
    • 系统类加载器 尝试加载。
    • 公共类加载器 尝试加载,加载位于 common 目录下的共享类库。
    • Catalina 类加载器 尝试加载 Tomcat 的内部类。
    • 共享类加载器 尝试加载共享类库(如果配置)。
    • Web 应用类加载器 尝试加载 WEB-INF/classes 和 WEB-INF/lib 下的类和资源。

5 JVM垃圾回收

对象存活算法

  • 引用计数法:
    • 给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
    • 优点:实现简单,判定效率也很高
    • 缺点:难解决对象之间相互循环引用的问题
  • 可达性分析法:
    • 通过一系列的成为“GC Roots”(活动线程相关的各种引用,虚拟机栈帧引用,静态变量引用,JNI引用)的对象作为起始点,从这些节点ReferenceChains开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的;
  • 两次标记过程:
    • 对象被回收之前,该对象的finalize()方法会被调用;两次标记,即第一次标记不在“关系网”中的对象。第二次的话就要先判断该对象有没有实现finalize()方法了,如果没有实现就直接判断该对象可回收;如果实现了就会先放在一个队列中,并由虚拟机建立的一个低优先级的线程去执行它,随后就会进行第二次的小规模标记,在这次被标记的对象就会真正的被回收了。

垃圾回收算法

  • 垃圾回收算法:复制算法、标记清除、标记整理、分代收集;
  • 复制算法:(young)
    • 将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收;
    • 优点:实现简单,内存效率高,不易产生碎片
    • 缺点:内存压缩了一半,倘若存活对象多,Copying 算法的效率会大大降低
  • 标记清除:(cms)
    • 标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象
    • 缺点:效率低,标记清除后会产⽣⼤量不连续的碎⽚,需要预留空间给分配阶段的浮动垃圾
  • 标记整理:(old)
    • 标记过程仍然与“标记-清除”算法⼀样,再让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存;解决了产生大量不连续碎片问题
  • 分代收集:
    • 根据各个年代的特点选择合适的垃圾收集算法。
    • 新生代采用复制算法,新生代每次垃圾回收都要回收大部分对象,存活对象较少,即要复制的操作比较少,一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
    • 老年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
    • Safepoint 当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为 JVM 是安全的(safe),整个堆的状态是稳定的。如果在 GC 前,有线程迟迟进入不了 safepoint,那么整个 JVM 都在等待这个阻塞的线程,造成了整体 GC 的时间变长

垃圾收集器

  • 垃圾收集器截至JDK1.8,共分为7种:Serial收集器,ParNew收集器,Parallel Scavenge收集器,CMS收集器,Serial Old收集器,Parallel Old收集器,G1收集器(JDK1.8之后还有ZGC,Shenandoah)。
  • JDK3:Serial Parnew 关注效率
    • Serial:Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。适合用于客户端垃圾收集器。
    • Parnew:ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。
  • JDK5:parallel Scavenge+(Serial old/parallel old)关注吞吐量
    • parallel Scavenge:(关注吞吐量):Parallel Scavenge收集器关注点是吞吐量(⾼效率的利⽤CPU)。CMS等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验);高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
    • Serial old:Serial收集器的⽼年代版本,它同样是⼀个单线程收集器,使用标记-整理算法。主要有两个用途:
      • 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
      • 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
    • parallel old:Parallel Scavenge收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。
  • JDK8-CMS:(关注最短垃圾回收停顿时间)
    • CMS收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
      • 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,STW。
      • 并发标记:进行 ReferenceChains跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
      • 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,STW。
      • 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
    • 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
    • 优点:并发收集、低停顿
    • 缺点:
      • 对CPU资源非常敏感;
      • 无法处理“浮动垃圾”,可能会出现Concurrent Mode Failure(在并发清除时用户线程也在运行,自然就会同时产生新的垃圾,这些垃圾只能留在下次清理,称为“浮动垃圾”),所以并发清楚时需要预留一定内存空间(给“浮动垃圾”),若预留的空间无法满足程序需要,则发生Concurrent Mode Failure,启动后备预案,临时启用Serial Old收集器,导致有一次Major GC);
      • 产生大量的内存碎片,标记—清除算法导致的。
  • JDK9-G1:(精准控制停顿时间,避免垃圾碎片)Garbage First
    • 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器.以极⾼概率满⾜GC停顿时间要求的同时,还具备⾼吞吐量性能特征;相比与 CMS 收集器,G1 收集器两个最突出的改进是:
      • 基于标记-整理算法,不产生内存碎片。
      • 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
    • G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
    • 整个过程分为4步:初始标记,并发标记,最终标记,筛选回收;
      • 初始标记:Stop The World,仅使用一条初始标记线程对GC Roots关联的对象进行标记
      • 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢
      • 最终标记:Stop The World,使用多条标记线程并发执行
      • 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行
    • 有以下4个特点:
      • 并行与并发:可以并行执行以减少Stop The World的停顿时间,也可以并发的执行垃圾回收线程与用户线程。
      • 分代收集:虽然保留了分代(新生代与老年代)概念,但G1将堆分为多个大小相等的独立区域(Region)。
      • 空间整合:从整体上看是使用的标记—整理算法,实际上是两个Region之间的“复制”算法。
      • 可预测的停顿:G1可以建立可预测的停顿时间模型是因为G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这也是G1名字中First(价值优先)的含义。
  • JDK11-ZGC:
    • JDK 11中引入的ZGC(Z Garbage Collector)是一个可伸缩的低延迟垃圾收集器。ZGC的目标是支持使用TB级别内存容量的系统,同时保持垃圾收集过程中的停顿时间不超过10毫秒。以下是ZGC的几个关键特点:
      • 并发性:ZGC大部分的垃圾收集操作都是并发执行的,这意味着程序在垃圾收集过程中大部分时间仍然在运行,只有少数必要的操作会导致短暂的停顿。
      • 低延迟:ZGC的设计目标是将停顿时间控制在10毫秒以内,无论是小规模还是大规模的堆,这对低延迟应用程序非常重要。
      • 可伸缩性:ZGC旨在以几百兆到几个TB的堆大小都能保持相同的垃圾收集性能,从而支持高容量应用程序。
      • 无内存碎片:ZGC通过使用等大的内存页以及压缩指针(colored pointers)和加载障碍技术(load barriers)来减少内存碎片。
  • ZGC的垃圾收集过程分为几个主要阶段:
    • 标记阶段(Mark Start):这是一个STW(Stop-The-World)阶段,但通常很短。在这个阶段,ZGC初始化必要的数据结构,并标记从根集合开始的直接可达对象。
    • 并发标记阶段:系统继续运行,而ZGC的线程并发地标记所有可达对象。这包括对象图遍历的并发执行。
    • 重新定位阶段(Relocate Start):ZGC会暂停应用线程,主要处理一些必要的预处理工作。
    • 并发重新定位阶段:在这个阶段,ZGC并发地移动对象以消除内存碎片。对象移动后,ZGC会更新所有指向这些对象的引用。
    • 清理阶段:在内存回收操作完成后,ZGC会清除不再使用的内存页。

JDK新特性

  • JDK8:支持 Lamda 表达式、集合的 stream 操作、提升HashMap性能
  • JDK9:主要是API的优化,如支持HTTP2的Client API、JVM采用G1为默认垃圾收集器,java模块化
  • JDK10:并行全垃圾回收器 G1,来优化G1的延迟,通过var关键字实现局部变量类型推断,使Java语言变成"弱类型"语言、JVM的G1垃圾回收由单线程改成多线程并行处理,降低G1的停顿时间
  • JDK11:ZGC (并发回收的策略) 4TB,用于 Lambda 参数的局部变量语法,Stream、集合等API的增强
  • JDK12:Shenandoah GC (GC 算法)停顿时间和堆的大小没有任何关系,并行关注停顿响应时间。switch表达式语法扩展、G1收集器优化;
  • JDK13:增加ZGC以将未使用的堆内存返回给操作系统,16TB,socket底层实现引入NIO
  • JDK14:删除cms垃圾回收器、弃用ParallelScavenge+SerialOldGC垃圾回收算法组合。将ZGC垃圾回收器应用到macOS和windows平台
  • JDK15;
  • JDK16:相当于是将JDK14、JDK15的一些特性进行了正式引入,如instanceof模式匹配(Pattern matching)、record的引入等最终到JDK16变成了final版本
  • JDK17;

6 线上故障排查

JVM常用命令

  • 对应进程的JVM状态以定位问题和解决问题并作出相应的优化
  • 常用命令:jps、jinfo、jstat、jstack、jmap
  • jps:查看java进程及相关信息
    • $jps -v
    • jps -l 输出jar包路径,类全名
    • jps -m 输出main参数
    • jps -v 输出JVM参数
  • jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收
    • jstat [option] LVMID [interval] [count]其中LVMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印)
      • $jstat -gc 4245 500 5
    • option参数解释:
      • -gc 垃圾回收堆的行为统计
      • -gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计
      • -gcutil 垃圾回收统计概述
      • -gcnew 新生代行为统计-gcold 年老代和永生代行为统计
      • jstat –gccause 查询GC的原因
  • jinfo:查看JVM参数
    • $jinfo 4245
    • $jinfo -flags 4245
  • jmap:可以用来查看内存信息(配合jhat使用)
    • 命令格式:jmap [option] (连接正在执行的进程) 不加参数默认打印所有
    • option参数解释:
      • -heap 打印java heap摘要
      • -histo[:live] 打印堆中的java对象统计信息
      • -clstats 打印类加载器统计信息
      • -finalizerinfo 打印在f-queue中等待执行finalizer方法的对象
      • -dump: 生成java堆的dump文件
  • jstack:查看JVM线程快照,jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环
    • 命令格式:jstack [options]
    • option参数解释:
      • -F 当使用jstack 无响应时,强制输出线程堆栈。
      • -m 同时输出java堆栈和c/c++堆栈信息(混合模式)
      • -l 除了输出堆栈信息外,还显示关于锁的附加信息,如死锁。
  • jhat是用来分析jmap生成dump文件的命令,jhat内置了应用服务器,可以通过网页查看dump文件分析结果,jhat一般是用在离线分析上。
    • 命令格式 : jhat [option][dumpfile]
    • option参数解释:
    • -stack false: 关闭对象分配调用堆栈的跟踪
    • -refs false: 关闭对象引用的跟踪
    • -port : HTTP服务器端口,默认是7000 -debug : debug级别
    • -version 分析报告版本

内存溢出

  • 大数据量,无可用内存分配
  • 内存泄漏,没及时回收。
  • JVM内存容量参数分配不合适。
  • 代码bug,死循环无限期分配容量。
参数描述
-Xms初始堆
-Xmx最大堆大小
-Xmn新生代的内空间.空间包含(eden+2个survivor),默认配比是8:1:1
-XX:SurvivorRatioEden区与Survivor区的大小比值
-XX:PermSize永久代的分配
-XX:MaxPermSize永久代最大值
-Xss:每个线程堆栈的大小

CPU飙高

  • CPU 的使用会飙升,一旦飙升,一般怀疑某个业务逻辑的计算量太大,或者是触发了死循环(比如著名的 HashMap 高并发引起的死循环),但排查到最后其实是 GC 的问题。
    • 使用 top 命令,查找到使用 CPU 最多的某个进程,记录它的 pid。使用 Shift + P 快捷键可以按 CPU 的使用率进行排序。
      • top
    • 再次使用 top 命令,加 -H 参数,查看某个进程中使用 CPU 最多的某个线程,记录线程的 ID。
      • top -Hp $pid
    • 使用 printf 函数,将十进制的 tid 转化成十六进制。
      • printf %x $tid
    • 使用 jstack 命令,查看 Java 进程的线程栈。
      • jstack p i d > pid > pid>pid.log
    • 使用 less 命令查看生成的文件,并查找刚才转化的十六进制 tid,找到发生问题的线程上下文。
      • less $pid.log
    • jstack 日志搜关键字DEAD,以及中找到了 CPU 使用最多的几个线程id。
      • 堆已经满了,GC 进程一直回收,回收的效果又非常一般,造成 CPU 升高应用假死。
    • 具体问题排查,就需要把内存 dump 一份下来,使用 MAT 等工具分析具体原因了。
    • 其它方法:arthas(http://mw.alibaba-inc.com/products/arthas/docs/middleware-container/arthas.wiki/home.html?spm=a1z9z.8109794.header.32.1lsoMc)
$ps -ef | grep java

$top -Hp 4245

$printf “%x\n” 529714b1n”

$jstack 4245 | grep 14b1n

$jstack 4245


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 如何查线程最耗费CPU的线程信息。
    • $ps -ef | grep java
    • jps 查找出java进程ID
  • top 命令,加 -H 参数,查看某个进程中使用 CPU 最多的某个线程,记录线程的 ID。
    • top -Hp $pid
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/930955
推荐阅读
相关标签
  

闽ICP备14008679号