JVM笔记

JVM探究

  • 请你谈谈你对JVM的理解? java ---> class ---> JVM
  • java8 虚拟机和之前的变化,有什么更新?
  • 什么是OOM(out of memery,内存溢出),栈溢出(StackOverFlowError)?怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取?怎么分析Dump文件?
  • 谈谈JVM中,类加载器你的认识?

1.JVM的位置

1648630379815

2.JVM的体系结构

1648647550809

1648647585515

补:字节码文件

先简单介绍以下相关操作指令:

iconst_1 表示将值为1的数压入操作数栈;

istore_1 表示从操作数栈中弹出一个值,存放在局部变量表index为1的位置(index为0的位置存放着this);

iload_1 表示将局部变量表中,index为1位置的值,压入到操作数栈中;

ifeq 14 表示条件不满足时跳转到偏移量为14的位置;

getstatic 表示访问类的静态字段;

if_icmpne 27 表示条件比较,不满足时跳转到偏移量为27的位置;

ldc 表示将常量加载到操作数栈中;

invokevirtual 表示调用对象的虚方法

return 表示方法返回;

java字节码指令集 - VinoZhu - 博客园 (cnblogs.com)

3.类加载器

类的加载过程:加载、验证、准备、解析、初始化

1654765847896

加载:

  • 通过一个类的全限定名来获取其定义的二进制字节流。

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

1654766033258

验证:确保被加载的类的正确性,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:为类的静态变量分配内存,并将其初始化为默认值。这些内存都将在方法区中分配。

  • 这时候进行内存分配的仅包括static变量,不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 这里的初始值是 Java 数据类型的默认零值(0、0L、null等),而不是Java代码中显式赋予的值。
  • 同时被final和static修饰的变量,必须在声明的时候就为其显式地赋值,且在准备阶段虚拟机就会赋值。

解析:把类中的符号引用转换为直接引用

符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

类加载器作用:加载 Class 文件

1648642187733

  1. 虚拟机自带加载器

    启动类加载器(C++实现):负责加载存放在JDK\jre\lib下,或被-Xbootclasspath参数指定的路径中的类库

  2. 其他的类加载器

    扩展类加载器:它负责加载JDK\jre\lib\ext目录中的所有类库

    应用程序加载器:它负责加载用户类路径(ClassPath)所指定的类

类的加载:

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载,加载含有 main 的主类
  2. 通过Class.forName()方法动态加载,默认会执行类中的 static 块
  3. 通过ClassLoader.loadClass()方法动态加载

类加载机制:

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象。
  • 双亲委派机制

1648642672452

4.双亲委派机制

  • APP ---> EXT ---> BOOT(最终执行)
    • Bootstrap ClassLoader:仅用于rt.jar,最高优先级将给予此加载程序
    • Extension ClassLoader:加载jre\lib\ext文件夹内的类
    • Application ClassLoader:负责加载应用程序级别的类路径
  1. 类加载器收到类加载的请求
  2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
  3. 启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
  4. 重复步骤3

从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到上级加载器,然后调用上级加载器的loadClass方法。上级类加载器中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有上级加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到下级加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException

优点:

  1. 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  2. 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全

5.沙箱安全机制

java中的安全模型(沙箱机制)_改变ing的博客-CSDN博客_沙箱安全机制

6.Native

编写一个多线程类启动

public static void main(String[] args) {
    new Thread(()->{
        
    },"your thread name").start();
}

//点开start()方法:
//private native void start0();
  • native:凡是带了 native 关键字的,说明 java 的作用范围达不到了,会去调用底层C语言的库
  • 会进入本地方法栈 调用本地方法接口JNI
  • JNI 作用:扩展 Java 的使用,融合不同的编程语言为 Java 所用
  • 它在内存区域专门开辟了一块标记区域:Native Method Stack,登记 native 方法
  • 在最终执行的时候,加载本地方法库中的方法通过 JNI

JNI:Java Native Interface(Java 本地方法接口)

Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。

Native Method Stack(本地方法栈)

它的具体做法是:Native Method Stack 中登记 native 方法,在 Execution Engine(执行引擎)执行的时候加载 Native Libraries(本地库)。

7.PC寄存器

程序计数寄存器(Program Counter Register),简称PC寄存器。

  1. PC寄存器是用来存储指向下一条指令的地址,也即将将要执行的指令代码。由执行引擎读取下一条指令。
  2. 它是一块很小的内存空间,几乎可以忽略不计。可以看作是当前线程所执行的字节码的行号指示器
  3. 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
  4. 它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域

8.方法区

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

  1. 类型信息(类class、接口interface、枚举enum、注解annotation)

    类模板Class、类的构造方法、接口定义等

  2. 域信息(理解为成员变量)

    域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient 的某个子集)

  3. 方法信息

    每个方法的名字、返回类型、参数类型(按顺序)、修饰符、属性

  4. 运行时常量池

    • 编译期生成的各种字面量和符号引用(类引用、字段引用、方法引用、接口方法引用),即常量池表,将在类加载后存放到方法区的运行时常量池中
    • 相比于Class文件常量池,运行时常量池具有动态性,运行期间也可以将新的常量放入池中
  5. 静态变量、常量

    实例变量存在堆内存中

    JDK1.7 之后,静态变量字符串常量池 存储在堆中了

1654938381294

方法区的垃圾回收:

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

方法区内常量池之中主要存放的两大类常量:字面量符号引用

  • 字面量:常量,如文本字符串、被声明为 final 的常量值等
  • 符号引用:编译期间产生的
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

对于常量的回收,只要常量池中的常量没有被任何地方引用,就可以被回收

对于类的回收,需要满足三个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例

  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常很难达成

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

9.栈

数据结构

栈:先进后出、后进先出

队列:先进先出(FIFO,First Input First Output)

为什么main()先执行,最后结束?

无限递归,栈溢出

栈:

  • 栈内存,主管程序的运行,生命周期和线程同步;
  • JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
  • 线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收问题。

栈中存放:8大基本类型+对象引用+实例方法

注意:

成员变量、局部变量、类变量分别存储在内存的什么地方?

类变量

  • 类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁
  • 在java8之前把静态变量存放于方法区,在java8时存放在堆中

成员变量

  • 成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
  • 由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量

  • 局部变量是定义在类的方法中的变量
  • 在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中

栈运行原理:

  • JVM 直接对 Java 栈的操作只有两个,对栈帧的压栈出栈,遵循“先进后出/后进先出”原则

  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)

  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作

  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧

  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧

  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧

  • Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令,另一种是抛出异常,不管用哪种方式,都会导致栈帧被弹出

栈帧:

  1. 局部变量表:用于存储方法参数和定义在方法体内的局部变量。

    • 局部变量表最基本的存储单元是 Slot(变量槽)

    • 栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的

    • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

    • JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引

  2. 操作数栈:在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容。主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

    • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令

    • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问

  3. 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了保证方法调用过程中实现动态连接。

    简单来讲就是:符号引用就是先有一个标签。第一次运行后将这个标签替换为一个可以直接找到方法具体内存位置的具体值,利用这个具体值可以直接将被调用的方法直接放到虚拟机栈内存。

  4. 返回地址:用来存放调用该方法的 PC 寄存器的值。

    当一个方法开始执行后,只有2种方式可以退出这个方法 :方法返回指令和异常退出。无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。

    正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

1648696488526

栈 + 堆 + 方法区:交互关系

对象实例化过程

1648697412113

10.三种JVM

  • Sun公司 HotSpot
  • BEA JRockit
  • IBM J9 VM

11.堆

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把什么东西放到堆中?实例对象、数组、类的非静态成员变量

堆内存中还可以细分为三个区域:

  • 新生代

    • 一个对象 诞生、成长甚至死亡的地方;
    • 伊甸园区:所有的对象都是在 伊甸园区 new出来的!
    • 幸存者区(0,1)
    • 默认内存比例为 8 : 1 : 1,可以通过 -XX:SurvivorRatio 来配置
  • 老年代

    • 新生代与老年代内存比例 2 :1,可以通过 –XX:NewRatio 来配置
  • 永久代

    这个区域是常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭VM就会释放这个区域的内存。

    • jdk1.6 之前:永久代,常量池在方法区
    • jdk1.7:永久代,但是慢慢退化了(去永久代),常量池在堆中
    • jdk1.8之后:没有永久代,常量池在元空间。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,即逻辑上存在,物理上不存在(非堆)。

1648704550686

1648733496444

对象在堆中的生命周期:

  1. 在 JVM 内存模型的堆中,堆被划分为新生代和老年代

    • 新生代又被进一步划分为 Eden区Survivor区,Survivor 区由 From SurvivorTo Survivor 组成
  2. 当创建一个对象时,对象会被优先分配到新生代的 Eden 区

    • 此时 JVM 会给对象定义一个对象年轻计数器-XX:MaxTenuringThreshold
  3. 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)

    • JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
    • 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
  4. 如果分配的对象超过了-XX:PetenureSizeThreshold,对象会直接被分配到老年代

GC 垃圾回收,主要是在伊甸园区和老年代。

假设内存满了,OOM,堆内存不够!

  • 尝试扩大堆内存看结果 -Xms1024m -Xmx1024m -XX:+PrintGCDetails
  • 分析内存,看一下哪个地方出现了问题(专业工具)
public class Test {
    public static void main(String[] args) {
        //返回虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        //返回jvm的初始化总内存
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max=" + max + "字节\t" + (max/(double)1024/1024) + "MB");
        System.out.println("total=" + total + "字节\t" + (total/(double)1024/1024) + "MB");

        //默认情况下:分配的总内存 是电脑内存的1/4,而初始化的内存是1/64

        //OOM:
        //1.尝试扩大堆内存看结果  -Xms1024m -Xmx1024m -XX:+PrintGCDetails
        //分析内存,看一下哪个地方出现了问题(专业工具)

    }
}

305664K(YoungGen) + 699392K(OldGen) = 1005056K = 981.5M(total)

所以,元空间物理上不在堆内存中!

1648787233649

12.堆内存调优

在一个项目中,突然出现了OOM故障,那么应该如何排除,研究为什么出错

  • 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
    • MAT,Jprofiler作用:
      • 分析Dump内存文件,快速定位内存泄露问题
      • 获得堆中的数据
      • 获得大的对象
      • ...
  • Debug,一行行分析代码

Dump文件产生命令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

import java.util.*;
// -Xms 设置初始化内存分配大小 1/64
// -Xmx 设置最大分配内存,默认1/4
// -XX:+PrintGCDetails 打印GC垃圾回收信息
// -XX:+HeapDumpOnOutOfMemoryError  OOM Dump

// -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class Test {
    byte[] array = new byte[1 * 1024 * 1024]; //1M

    public static void main(String[] args) {
        ArrayList<Test> list = new ArrayList<>();
        int count = 0;

        try {
            while (true) {
                list.add(new Test());
                count = count + 1;
            }

        }catch (Error e) {
            System.out.println("count:" + count);
            e.printStackTrace();
        }

    }
}

13.GC 垃圾回收

JVM在进行垃圾回收时,并不是对这三个区域统一回收。大部分时候,回收都是伊甸园区

  • 伊甸园区
  • 幸存区
  • 老年代

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。
    • 需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
    • 目前,只有 CMS GC 会有单独收集老年代的行为
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
    • 目前只有 G1 GC 会有这种行为

整堆收集 (Full GC):收集整个 Java 堆和方法区。

Full GC 触发条件:

  • 调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。
  • 老年代空间不足
  • 空间分配担保失败:使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
  • Concurrent Mode Failure:报 Concurrent Mode Failure 错误,并触发 Full GC

GC题目:

  • JVM的内存模型和分区~详细到每个区放什么?
  • 堆里面的分区有哪些?Eden、From、To、OldGen,说说他们的特点!
  • GC的算法有哪些?标记清楚法、标记整理法、复制算法、分代收集算法,怎么用的?
  • 轻GC 和 重GC 分别在什么时候发生?

判断对象是否死亡算法

主要有两种算法:引用计数法可达性分析算法

引用计数法

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

1648792440209

可达性分析算法:

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

1654953756749

所谓 GC Roots 就是一组必须活跃的引用,一般包含以下内容:

  • 虚拟机栈中引用的对象
    • 栈中的局部变量
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
    • 类的引用类型静态变量
  • 方法区中的常量引用的对象
    • 字符串常量池里的引用
  • 所有被同步锁synchronized持有的对象
  • 针对分代收集和局部回收时,例如垃圾回收新生代,此时老年代里的引用对象可以作为GC Roots

finalize():

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。

引用类型

Java 具有四种强度不同的引用类型:

  • 强引用:被强引用关联的对象不会被回收。

    • 例如:new 一个对象,实现引用

      Object obj = new Object();
      
  • 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。

    • 使用 SoftReference 类来创建软引用。

      Object obj = new Object();
      SoftReference<Object> sf = new SoftReference<Object>(obj);
      obj = null;  // 使对象只被软引用关联
      
  • 弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

    • 使用 WeakReference 类来实现弱引用。

      Object obj = new Object();
      WeakReference<Object> wf = new WeakReference<Object>(obj);
      obj = null;
      
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。

    • 为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。

    • 使用 PhantomReference 来实现虚引用。

      Object obj = new Object();
      PhantomReference<Object> pf = new PhantomReference<Object>(obj);
      obj = null;
      

垃圾回收算法

复制算法

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

新生代采用复制算法,在回收时,将 Eden 和 Survivor From中还存活着的对象一次性复制到另一块 Survivor To空间上,最后清理 Eden 和使用过的那一块 Survivor From。

1648793172388

1648795929013

  • 好处:没有内存碎片
  • 坏处:浪费了内存空间,多了一半空间永远是空的(To)

复制算法最佳使用场景:对象存活度较低的时候;新生区~

标记清除法

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

1648796280617

  • 优点:不需要额外的空间!
  • 缺点:两次扫描严重浪费时间,会产生内存碎片

标记压缩法

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

1648796559400

总结

内存效率:复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

思考一个问题:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的算法 ---> GC:分代收集算法

年轻代:

  • 存活率低
  • 复制算法!

老年代:

  • 区域大,存活率高
  • 标记清除 + 标记压缩混合实现

垃圾收集器

单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;

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

并发:垃圾回收线程可以与用户线程交替执行,不用暂停用户线程

两大指标:

  • 吞吐量:运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
    • 高效利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务
  • 暂停时间:暂停用户线程,运行GC的时间
    • 注重低延迟,用户交互

1655033262223

1655033569224

1655033635159

JDK1.8删除红色虚线

JDK1.10 删除绿色虚线

JDK1.14 删除青色虚框

Parallel Scavenge GC + Parallel Old GC 是 JDK1.8 默认垃圾回收器

G1 是JDK1.9及以上 默认垃圾回收器

-XX:+PrintCommandLineFlags :显示正在使用的GC收集器

1655116581344

Serial 收集器

串行、单线程收集器

采用复制算法、串行回收 和 “stop the world”机制进行回收

简单高效

-XX:+UseSerialGC :指定新生代使用Serial GC,同时老年代使用Serial Old GC

ParNew 收集器

New 代表新生代并行回收

并行、多线程

Serial 收集器的多线程版本。

采用复制算法、“stop the world”机制

-XX:+UseSerialGC :指定使用ParNew垃圾回收器

-XX:ParallelGCThreads:限制线程数量,默认开启和CPU数据相同的线程数

Parallel Scavenge 收集器

并行、多线程

目标是达到一个可控制的吞吐量,被称为“吞吐量优先”收集器。

可以自适应调节,即会自动调节年轻代大小、Eden和survivor的比例、晋升老年代的对象年龄等参数

吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。

复制算法、并行回收、“stop the world”机制

-XX:+UseParallelGC :指定年轻代使用Parallel Scavenge收集器

-XX:UseParallelOldGC:指定老年代使用Parallel Old GC,上面二者可以互相激活

-XX:ParallelGCThreads:设置垃圾收集器线程数

Serial Old 收集器

串行、单线程

Serial 收集器的老年代版本。

标记-压缩算法、串行回收、“stop the world”机制

  • Client模式下默认的老年代垃圾回收器
  • Servier模式下:
    • 与新生代的Parallel Scavenge配合使用
    • 作为老年代CMS收集器的后备垃圾收集方案

Parallel Old 收集器

并行、多线程

是 Parallel Scavenge 收集器的老年代版本

标记-压缩算法、并行回收、“stop the world”机制

CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法(会产生内存碎片)、“stop the world”机制

第一次实现了垃圾收集线程和用户线程同时工作。

目标:尽可能缩短垃圾收集时用户线程的停顿时间

分为以下四个流程:

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

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

为什么不能使用标记-压缩算法?

因为在进行垃圾回收的时候,用户线程与垃圾回收线程是并发执行的,标记-压缩算法需要对对象进行重新分配内存,而此时用户线程正在执行,会受到影响。

优点:

  • 并发收集
  • 低延迟

缺点:

  • 会产生内存碎片
  • 对CPU资源十分敏感:虽然不会导致用户线程停顿,但是会因为占用一部分线程而导致应用程序变慢,总吞吐量降低。
  • 无法处理浮动垃圾:原因是因为重新标记不能取消标记
    • 并发标记:标记可达对象(非垃圾)
    • 重新标记:标记新的可达对象(非垃圾)
    • 浮动垃圾:在并发标记阶段标记过的非垃圾,变成了垃圾,重新标记阶段无法取消标记,变成了浮动垃圾

-XX:+UseConcMarkSweepGC:指定使用CMS收集器,新生代会自动执行-XX:+UseParNewGC,即ParNew(新生代)+ CMS(老年代)+Serial Old(备用)

G1 收集器

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

目标:延迟可控的情况下获得尽可能高的吞吐量

Garbage First:优先回收价值大的垃圾

特点:

  • 并行与并发:
    • 并行:多个垃圾线程同时工作
    • 并行:垃圾线程和用户线程交替执行
  • 分代收集:堆空间被分为若干Region,这些区域逻辑上包含了年轻代和老年代
  • 空间整合:region之间是复制算法,整体上是标记-压缩算法
  • 可预测的停顿时间模型(软实时):尽可能在指定STW时间内完成垃圾回收

G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记

  • 并发标记

  • 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。

  • 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

1655109954872

Region:

将Java堆划分成约2048个大小相同的独立Region,Region块大小根据堆空间大小而定。所有Region大小相同,且在JVM生命周期内不会改变。

G1对内存的使用以分区(Region)为单位,而对对象的分配则以卡片(Card)为单位

1655108521558

注:上图修改为 “超过0.5个region”

Remembered Set(RSet):

记录对象的引用,避免进行全局扫描,解决分代引用问题。

并非所有引用都需要记录在RSet中:

  • 确定要扫描的分区中的引用,无需记录
  • G1 GC每次都会对年轻代进行整体扫描,年轻代中的引用,无需记录

1655112323224

1655112451143

1655115883608

1655116156902

1655116134039

并发标记:三色标记法

  • 白色:表示未被标记的对象

  • 灰色:表示自己被标记,但是引用对象还未被标记

  • 黑色:表示自己被标记了,引用对象也全部标记完成

GC 开始前所有对象都是白色,GC 一开始所有根能够直达的对象被压到栈中,待搜索,此时颜色是灰色。然后灰色对象依次从栈中取出搜索子对象,子对象也会被涂为灰色,入栈。当其所有的子对象都涂为灰色之后该对象被涂为黑色。当 GC 结束之后灰色对象将全部没了,剩下黑色的为存活对象,白色的为垃圾。

漏标问题:

在 remark 的过程中,如果黑色标记对象指向了白色标记对象,若不对黑色标记对象重新扫描,则白色标记会漏标。白色对象会被当作没有任何引用而被清理掉。

产生漏标的条件有两个:

  • 黑色对象指向了白色对象
  • 灰色对象指向白色对象的引用消失

解决漏标的问题,打破两个条件之一就可以:

  • 跟踪黑指向白的增加:incremental update,增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS采用该方法。
  • 记录灰指向白的消失:snapshot at the beginning(SATB),关注引用的删除,当灰–>白消失时,要把这个 引用 推到GC的堆栈,保证白还能被GC扫描到。G1采用该方法。

G1若使用第一种方案,将黑重新标记为灰对象,需要再重新扫描一遍,效率低。SATB配合RSet,只需要扫描RSet,效率较高。

转移失败的担保机制 Full GC:G1在以下场景中会触发Full GC

  • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  • 从老年代分区转移存活对象时,无法找到可用的空闲分区
  • 分配巨型对象时在老年代无法找到足够的连续分区

ZGC

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延迟垃圾回收器。

CMS和G1停顿时间瓶颈

CMS和G1新生代垃圾回收主要采用标记-复制算法,主要包括3个阶段:标记阶段、转移阶段和重定位阶段。以G1为例,混合回收主要包括:标记阶段、清理阶段和复制阶段。

image-20220622155521688

标记阶段STW分析:

  • 初始标记阶段:从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
  • 并发标记阶段:不是STW,所以我们不太关心该阶段耗时的长短。
  • 再标记阶段:新标记那些在并发标记阶段发生变化的对象。该阶段是STW的,但因为对象数少,耗时也较短。

清理阶段STW分析:

清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。因为内存分区数量少,耗时也较短。

复制阶段STW分析:

需要分配新内存和复制对象的成员变量。转移阶段是STW的,由于要处理所有存活的对象,耗时会较长

G1未能解决转移过程中准确定位对象地址的问题,因此转移阶段不能并发执行。

ZGC原理

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

image-20220622155609657

ZGC只有三个STW阶段:初始标记,再标记,初始转移。初始标记和初始转移分别都只需要扫描所有GC Roots,耗时非常短;再标记阶段STW时间也很短。ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。

ZGC关键技术

着色指针读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。

  • 着色指针:即对象引用的地址。利用着色指针可以判断对象被移动过。
  • 读屏障:应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。仅“从堆中读取对象引用”才会触发

14.JMM

什么是JMM?

Java Memory Model,Java内存模型

JVM内部使用的Java内存模型在线程栈和堆之间划分内存。

  • 线程只能访问它自己的线程栈,由线程创建的局部变量对于创建它的线程以外的所有其他线程是不可见的。
  • 堆包含了在Java应用程序中创建的所有对象,所有具有对象引用的线程都可以访问堆上的该对象。

现代硬件内存架构与内部Java内存模型略有不同,包含有CPU寄存器、高速缓存存储器和主内存。

在硬件上,线程栈和堆都位于主存储器中。线程栈和堆的一部分有时可能存在于CPU高速缓存和内部CPU寄存器中。因此,当对象和变量可以存储在计算机的各种不同存储区域中时,可能会出现两个问题:

  • 线程更新(写入)共享变量的可见性
  • 读取、检查和写入共享变量时的竞争条件

可见性问题:

如果两个或多个线程共享一个对象,则一个线程对共享对象的更新可能对其他线程不可见。

JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

1648798241361

解决共享对象可见性这个问题:volatile关键字。可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

竞态条件问题:

如果两个或多个线程共享一个对象,并且多个线程更新该共享对象中的变量,则可能会出现竞态。

要解决此问题,您可以使用Java synchronized块。

Java内存模型是围绕着并发编程中原子性可见性有序性这三个特征来建立的

  • 原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。(有点类似于事务操作)
  • 可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到该变量的这种变化。
  • 有序性:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。

volatile可以保证可见性和有序性;synchronized可以保证原子性

内存模型详解:

并发模式:

  • 共享内存:通过内存中的公共状态隐式进行通信
  • 消息传递:线程之间通过明确的发送消息显式进行通信

Java 并发采用的是共享内存模型。

Java 线程之间的通信由 Java 内存模型(JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本。

1654950149160

从上图来看,线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:

  • 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
  • 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

为了保证内存可见性,java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:

1654951434894

happens-before:

JSR -133 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。

与程序员密切相关的 happens-before 规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

  • volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。

  • 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

JMM 的内存可见性保证:

Java 程序的内存可见性保证按程序类型可以分为下列三类:

  • 单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime 和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

  • 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是 JMM 关注的重点,JMM 通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

  • 未同步 / 未正确同步的多线程程序。JMM 为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)

15.Java对象的创建过程

1、类加载检查

​ JVM在读取一条new指令的时候,首先检查能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化。如果没有,则会先执行相应的类加载过程。

2、内存分配

经过步骤1后,为新生对象分配内存,分配方式主要有两种,分别为:

  • 指针碰撞:即在开辟内存空间时候,将分界值指针往没用过的内存方向移动向应大小位置即可

    应用场合:堆内存规整

    将堆内存这样划分的代表的GC收集器算法有:Serial,ParNew

  • 空闲列表:虚拟机维护一个可以记录内存块是否可以用的列表来了解内存分配情况

    应用场合:堆内存不规整

    将堆内存这样划分的代表的GC收集器算法有:CMS

3、初始化默认值

将分配到内存的对象进行初始化,保证对象实例的字段在Java代码中可以在不赋初值的情况下使用。

4、设置对象头

在对象头中标记对象实例的基本信息,例如属于哪个类的实例,对象的hash码,对象年龄等

5、执行初始化方法

最后执行由开发人员编写的对象的初始化方法。

16.JVM调优

常用参数

JVM参数:

  • -Xms:堆内存最小值
  • -Xmx:堆内存最大值
  • -Xmn:新生代大小
  • -Xss:每个线程池的堆栈大小
  • -XX:NewRatio:设置新生代与老年代比值,-XX:NewRatio=4 表示新生代与老年代所占比例为1:4
  • -XX:PermSize:设置持久代初始值,默认是物理内存的六十四分之一, 在JDK8 已不能使用
  • -XX:MaxPermSize:设置持久代最大值,默认是物理内存的四分之一, 在JDK8 已不能使用
  • -XX:MaxTenuringThreshold:新生代中对象存活次数,默认15
  • -XX:SurvivorRatio:Eden区与Subrvivor区大小的比值,如果设置为8
  • -XX:+UseFastAccessorMethods:原始类型快速优化
  • -XX:+AggressiveOpts:编译速度加快
  • -XX:PretenureSizeThreshold:对象超过多大值时直接在老年代中分配

GC参数:

  • -XX:+UseSerialGC:使用串行垃圾回收
  • -XX:+UseParNewGC:新生代使用并行,老年代使用串行
  • -XX:+UseConcMarkSweepGC:新生代使用并行,老年代使用CMS
  • -XX:ParallelGCThreads:指定并行的垃圾回收线程的数量,最好等于CPU数量
  • -XX:+DisableExplicitGC:禁用System.gc(),因为它会触发Full GC,这是很浪费性能
  • -XX:CMSFullGCsBeforeCompaction:在多少次GC后进行内存压缩
  • -XX:+CMSParallelRemarkEnabled:降低标记停顿
  • -XX:+UseCMSCompactAtFullCollection:在每一次Full GC时对老年代区域碎片整理
  • -XX:+UseCmsInitiatingOccupancyOnly:使用手动触发或者自定义触发cms 收集
  • -XX:CMSInitiatingOccupancyFraction:使用CMS作为垃圾回收,使用70%后开始CMS收集
  • -XX:CMSInitiatingPermOccupancyFraction:设置perm gen使用达到多少%比时触发垃圾回收,默认是92%
  • -XX:+CMSIncrementalMode:设置为增量模式
  • -XX:+CmsClassUnloadingEnabled:CMS是不会默认对永久代进行垃圾回收的,设置此参数则是开启
  • -XX:+PrintGCDetails:开启详细GC日志模式
  • -XX:+PrintGCDateStamps:将时间和日期也加入到GC日志中

OOM(OutOfMemoryError)

堆内存溢出

Java 堆内存(Heap Memory)主要有两种形式的错误:

  1. OutOfMemoryError: Java heap space :堆内存无法继续存储对象
  2. OutOfMemoryError: GC overhead limit exceede:通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生

MetaSpace (元数据) 内存溢出

java.lang.OutOfMemoryError: Metaspace

不断创建类,使得元空间无法继续存储类信息。

posted @ 2022-11-28 17:39  柯文先生  阅读(102)  评论(0)    收藏  举报