• JVM虚拟机 总结很到位



    前言

    为什么要学习JVM?
    对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

    举例:比如我们经常要编写 HelloWord.java 电脑是怎么认识运行的?电脑只认识01010101

    Java文件编译的过程

    1. 程序员编写的 .java 文件
    2. 由 javac 编译成字节码文件 .class 文件(因为JVM只认识 .class 文件)
    3. 在由JVM编译成电脑认识的文件。
      在这里插入图片描述

    JDK、JRE、JVM三者的区别?
    通过下图我们可以看到,JDK包含了JRE,JRE包含了JVM。
    JRE大部分是C和C++语言编写的,他是我们在编译Java时所需要的基础类库。
    JDK 还包含了一些JRE以外的东西,就是这些东西帮我们编译Java代码的,还有就是监控JVM的一些工具。
    image.png

    内存结构

    说说JVM内存整体的结构?线程私有还是共享的?

    JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。

    共有五种:堆、方法区、虚拟机栈、程序计数器、本地方法栈

    概述他们的作用:

    • 堆:是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
    • ⽅法区:也是被线程共享的区域。在⽅法区中,存储了每个类的信息、静态变量、常量以及编译器编译后的代码等。
    • 虚拟机栈:Java栈中存放的是⼀个个的栈帧,每个栈帧对应⼀个被调⽤的⽅法。
    • 程序计数器:是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号),也就是用来计数的。
    • 本地⽅法栈:与栈类似,区别是栈为执⾏Java⽅法服务的,⽽本地⽅法栈则是为执⾏本地⽅法服务的

    线程私有:程序计数器,虚拟机栈,本地方法区。
    线程共享:堆,方法区,堆外内存(Java7的永久代或JDK8的元空间、代码缓存)
    image.png

    什么是程序计数器(线程私有)?

    PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。

    • PC寄存器为什么会被设定为线程私有的?

    多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。

    什么是虚拟机栈(线程私有)?

    主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。

    • 特点?
    1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
    2. JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
    3. 栈不存在垃圾回收问题
    4. 可以通过参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
    • 该区域有哪些异常
    1. 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
    2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常
    • 栈帧的内部结构?
    1. 局部变量表(Local Variables):主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用。
    2. 操作数栈(Operand Stack)(或称为表达式栈):主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
    3. 动态链接(Dynamic Linking):指向运行时常量池的方法引用

    1. 方法返回地址(Return Address):方法正常退出或异常退出的地址
    2. 一些附加信息

    什么是本地方法栈(线程私有)?

    • 本地方法接口

    一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。

    • 本地方法栈(Native Method Stack)

    Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用

    什么是方法区(线程共享)?

    方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。
    JDK1.8之前调节方法区大小:

    -XX:PermSize=N //方法区(永久代)初始大小
    -XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError 
    
    • 1
    • 2

    JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置:

    -XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
    -XX:MaxMetaspaceSize=N //设置Metaspace的最大大小
    
    • 1
    • 2

    栈、堆、方法区的交互关系

    永久代和元空间内存使用上的差异?

    Java虚拟机规范中只定义了方法区用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译后的代码等数据

    1. JDK1.7开始符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,但分离的并不彻底,此时永久代中还保存另一些与类的元数据无关的杂项
    2. JDK8后HotSpot 原永久代中存储的类的元数据将存储在metaspace中,而类的静态变量和字符串常量将放在Java堆中,metaspace是方法区的一种实现,只不过它使用的不是虚拟机内的内存,而是本地内存。在元空间中保存的数据比永久代中纯粹很多,就只是类的元数据,这些信息只对编译期或JVM的运行时有用。
    3. 永久代有一个JVM本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError
    4. 符号引用没有存在元空间中,而是存在native heap中,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。

    堆区内存是怎么细分的?

    对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。
    为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):

    1. 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代。
    2. 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大(占 2/3 的堆空间大小)


    Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。

    • 年轻代 (Young Generation)

    年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1

    1. 大多数新创建的对象都位于 Eden 内存空间中
    2. 当 Eden 空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中
    3. Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
    4. 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代
    • 老年代(Old Generation)

    旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。
    大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝

    JVM中对象在堆中的生命周期?

    1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代
    • 新生代又被进一步划分为 Eden区Survivor区,Survivor 区由 From SurvivorTo Survivor 组成
    1. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区
    • 此时 JVM 会给对象定义一个对象年轻计数器(-XX:MaxTenuringThreshold)
    1. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
    • JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
    • 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
    1. 如果分配的对象超过了-XX:PetenureSizeThreshold,对象会直接被分配到老年代

    JVM中对象的分配过程?

    为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。

    1. new 的对象先放在伊甸园区,此区有大小限制
    2. 当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
    3. 然后将伊甸园中的剩余对象移动到幸存者 0 区
    4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区
    5. 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
    6. 什么时候才会去养老区呢? 默认是 15 次回收标记
    7. 在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
    8. 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常

    GC垃圾回收

    如何判断一个对象是否可以回收?

    • 引用计数算法

    给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
    两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
    正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

    • 可达性分析算法

    通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

    Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

    • 虚拟机栈中引用的对象
    • 本地方法栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中的常量引用的对象

    对象有哪些引用类型?

    无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
    Java 具有四种强度不同的引用类型。

    • 强引用

    被强引用关联的对象不会被回收。
    使用 new 一个新对象的方式来创建强引用。Object obj = new Object();

    • 软引用

    被软引用关联的对象只有在内存不够的情况下才会被回收。
    使用 SoftReference 类来创建软引用。

    Object obj = new Object();
    SoftReference<Object> sf = new SoftReference<Object>(obj);
    obj = null;  // 使对象只被软引用关联
    
    • 1
    • 2
    • 3
    • 弱引用

    被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
    使用 WeakReference 类来实现弱引用。

    Object obj = new Object();
    WeakReference<Object> wf = new WeakReference<Object>(obj);
    obj = null;
    
    • 1
    • 2
    • 3
    • 虚引用

    又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
    为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知
    使用 PhantomReference 来实现虚引用。

    Object obj = new Object();
    PhantomReference<Object> pf = new PhantomReference<Object>(obj);
    obj = null;
    
    • 1
    • 2
    • 3

    有哪些基本的垃圾回收算法?

    标记 - 清除算法

    将存活的对象进行标记,然后清理掉未被标记的对象。
    不足:

    • 标记和清除过程效率都不高;
    • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

    标记 - 整理算法

    让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    标记- 复制算法

    将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

    主要不足是只使用了内存的一半。

    现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。

    HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

    分代收集

    现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
    一般将堆分为新生代和老年代。

    • 新生代使用:复制算法
    • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

    分代收集算法和分区收集算法区别?

    • 分代收集算法

    当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法。

    在新生代-复制算法:
    每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集。

    在老年代-标记整理算法:
    因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。

    1. ParNew: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。
    2. CMS: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除。
    • 分区收集算法

    分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次 GC 所产生的停顿。

    1. G1: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
    2. ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。

    什么是Minor GC、Major GC、Full GC?

    JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
    针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)

    • 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
      • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
      • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
        • 目前,只有 CMS GC 会有单独收集老年代的行为
        • 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
      • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
        • 目前只有 G1 GC 会有这种行为
    • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾

    说说JVM内存分配策略?

    • 对象优先在 Eden 分配

    大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

    • 大对象直接进入老年代

    大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
    经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
    -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

    • 长期存活的对象进入老年代

    为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
    -XX:MaxTenuringThreshold 用来定义年龄的阈值。

    • 动态对象年龄判定

    虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

    • 空间分配担保

    在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
    如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

    什么情况下会触发Full GC?

    对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

    • 调用 System.gc()

    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

    • 老年代空间不足

    老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
    为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

    • 空间分配担保失败

    使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

    • JDK 1.7 及以前的永久代空间不足

    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
    当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
    为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

    • Concurrent Mode Failure

    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

    Hotspot中有哪些垃圾回收器?


    以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

    • 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
    • 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

    1. Serial 收集器


    Serial 翻译为串行,也就是说它以串行的方式执行。
    它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
    它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
    它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

    2. ParNew 收集器


    它是 Serial 收集器的多线程版本
    是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
    默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

    3. Parallel Scavenge 收集器

    与 ParNew 一样是多线程收集器。
    其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。

    停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
    可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

    4. Serial Old 收集器


    是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:

    • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
    • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

    5. Parallel Old 收集器


    是 Parallel Scavenge 收集器的老年代版本。
    在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

    6. CMS 收集器


    CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
    分为以下四个流程:

    • 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
    • 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
    • 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
    • 并发清除: 不需要停顿。

    在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
    具有以下缺点:

    • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
    • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
    • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

    7. G1 收集器

    G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
    堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收

    G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

    通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
    每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

    如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

    • 初始标记
    • 并发标记
    • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
    • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

    具备如下特点:

    • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。

    类加载机制

    类加载的声明周期?

    其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

    • 类的加载:查找并加载类的二进制数据
    • 连接
      • 验证:确保被加载的类的正确性
      • 准备:为类的静态变量分配内存,并将其初始化为默认值
      • 解析:把类中的符号引用转换为直接引用
    • 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
    • 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据
    • 卸载: 结束生命周期

    image.png

    类加载器的层次?

    • 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
    • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
    • 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    • 自定义类加载器:因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
    • 在执行非置信代码之前,自动验证数字签名。
    • 动态地创建符合用户特定需要的定制化构建类。
    • 从特定的场所取得java class,例如数据库中和网络中。

    Class.forName()和ClassLoader.loadClass()区别?

    • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
    • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
    • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

    JVM有哪些类加载机制?

    • JVM类加载机制有哪些
    1. 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
    2. 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
    3. 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
    4. 双亲委派机制, 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
    • 双亲委派机制过程?
    1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
    2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
    3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
    4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

    JVM参数配置

    JVM内存参数简述

    常用的设置

    • -Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
    • -Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
    • -Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。
    • -XX:NewSize=n 设置年轻代初始化大小大小
    • -XX:MaxNewSize=n 设置年轻代最大值
    • -XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4
    • -XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8
    • -Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。
    • -XX:ThreadStackSize=n 线程堆栈大小
    • -XX:PermSize=n 设置持久代初始值
    • -XX:MaxPermSize=n 设置持久代大小
    • -XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

    下面是一些不常用的

    • -XX:LargePageSizeInBytes=n 设置堆内存的内存页大小
    • -XX:+UseFastAccessorMethods 优化原始类型的getter方法性能
    • -XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用
    • -XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动
    • -XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用
    • -Xnoclassgc 是否禁用垃圾回收
    • -XX:+UseThreadPriorities 使用本地线程的优先级,默认启用

    JVM的GC收集器设置

    • -XX:+UseSerialGC:设置串行收集器,年轻带收集器
    • -XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。
    • -XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量
    • -XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。
    • -XX:+UseConcMarkSweepGC:设置年老代并发收集器
    • -XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器

    JVM参数在哪设置

    1. 这里以IDEA 为例:

    image.png

    1. 全局配置
    • 找到IDEA安装目录中的bin目录
    • 找到idea.exe.vmoptions文件
    • 打开该文件编辑并保存。

    image.png

    1. war(Tomcat)包在哪里设置JVM参数

    war肯定是部署在Tomcat上的,那就是修改Tomcat的JVM参数

    • 在Windows下就是在文件/bin/catalina.bat
      增加如下设置:JAVA_OPTS(JAVA_OPTS,就是用来设置 JVM 相关运行参数的变量)

    image.png

    • Linux要在tomcat 的bin下的catalina.sh 文件里添加

    image.png
    注意:要在cygwin之前添加。

    1. Jar包在哪里设置JVM参数

    Jar包简单,一般都是SpringBoot项目打成Jar包来运行
    java -Xms1024m -Xmx1024m ...等等等 JVM参数 -jar springboot_app.jar &

    JVM调优总结

    1. 在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
    2. 初始堆值和最大堆内存内存越大,吞吐量就越高,但是也要根据自己电脑(服务器)的实际内存来比较。
    3. 最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。当然服务器一定要是多线程的
    4. 设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。默认的就是1:2
    5. 减少GC对老年代的回收。设置生代带垃圾对象最大年龄,进量不要有大量连续内存空间的java对象,因为会直接到老年代,内存不够就会执行GC

    JVM可视化工具

    为什么要用可视化工具

    开发大型 Java 应用程序的过程中难免遇到内存泄露、性能瓶颈等问题,比如文件、网络、数据库的连接未释放,未优化的算法等。随着应用程序的持续运行,可能会造成整个系统运行效率下降,严重的则会造成系统崩溃。为了找出程序中隐藏的这些问题,在项目开发后期往往会使用性能分析工具来对应用程序的性能进行分析和优化。

    visualVm

    VisualVM 是一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能为您提供强大的分析能力,对 Java 应用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析,同时,它能自动选择更快更轻量级的技术尽量减少性能分析对应用程序造成的影响,提高性能分析的精度。
    他作为Oracle JDK 的一部分,位于 JDK 根目录的 bin 文件夹下。VisualVM 自身要在 JDK6 以上的版本上运行,但是它能够监控 JDK1.4 以上版本的应用程序

    1. 打开visualVm

    位于 JDK 根目录的 bin 文件夹下的jvisualvm.exe
    image.png
    image.png

    1. 本地测试项目JVM运行状态

    image.png
    运行一个我们的SpringBoot项目
    image.png
    就可以监控到了,如下图:
    image.png

    jconsole

    从Java 5开始 引入了 JConsole。JConsole 是一个内置 Java 性能分析器,可以从命令行或在 GUI shell 中运行。您可以轻松地使用 JConsole(或者,它更高端的 “近亲” VisualVM )来监控 Java 应用程序性能和跟踪 Java 中的代码。

    1. 启动jconsole

    点击jdk/bin 目录下面的jconsole.exe 即可启动,然后会自动自动搜索本机运行的所有虚拟机进程。选择其中一个进程可开始进行监控image.png
    image.png

    参考:

  • 相关阅读:
    大数据到底是好是坏?_光点科技
    Linux 命令系统
    【附源码】Python计算机毕业设计培训中心管理系统
    Google 向中国开发者开放数百份 TensorFlow 资源
    划重点!3DEXPERIENCE SOLIDWORKS 2024 十大增强功能
    Folium 笔记:MarkerCluster
    milvus 相似度检索的底层原理
    【pytorch】torch.nn 与 torch.nn.functional 的区别
    C++输入输出总结
    【毕业设计】stm32单片机智能扫地机器人 - 嵌入式 物联网
  • 原文地址:https://blog.csdn.net/weixin_45606067/article/details/126296892