Java JVM

Java JVM

一、JVM结构

在这里插入图片描述

二、Class文件

public class Test {
    
    private int a;

    public void f1(){
        System.out.println(a);
    }
}

编译后的class文件:

//每4位代表2个字节
cafe babe 0000 0034 001e 0a00 0600 1009
0011 0012 0900 0500 130a 0014 0015 0700
1607 0017 0100 0161 0100 0149 0100 063c
696e 6974 3e01 0003 2829 5601 0004 436f
6465 0100 0f4c 696e 654e 756d 6265 7254
6162 6c65 0100 0266 3101 000a 536f 7572
6365 4669 6c65 0100 0954 6573 742e 6a61
7661 0c00 0900 0a07 0018 0c00 1900 1a0c
0007 0008 0700 1b0c 001c 001d 0100 0454
6573 7401 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0100 106a 6176 612f 6c61
6e67 2f53 7973 7465 6d01 0003 6f75 7401
0015 4c6a 6176 612f 696f 2f50 7269 6e74
5374 7265 616d 3b01 0013 6a61 7661 2f69
6f2f 5072 696e 7453 7472 6561 6d01 0007
7072 696e 746c 6e01 0004 2849 2956 0021
0005 0006 0000 0001 0002 0007 0008 0000
0002 0001 0009 000a 0001 000b 0000 001d
0001 0001 0000 0005 2ab7 0001 b100 0000
0100 0c00 0000 0600 0100 0000 0100 0100
0d00 0a00 0100 0b00 0000 2700 0200 0100
0000 0bb2 0002 2ab4 0003 b600 04b1 0000
0001 000c 0000 000a 0002 0000 0006 000a
0007 0001 000e 0000 0002 000f 
1.魔数

​ 文件开头的4个字节,标记文件的类型。class文件魔数为0xCAFFBABE

2.版本号

​ 后续的4个字节代表jdk版本号。0000是编译器jdk版本的次版本号,0034转化为十进制是52,是主版本号,Java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一,也就是说,编译生成该class文件的jdk版本为1.8.0。

在这里插入图片描述

反编译字节码文件

在这里插入图片描述

对class文件进行反编译:

C:\Users\XXX\Desktop>javap -v -p Test.class
Classfile /C:/Users/XXX/Desktop/Test.class //Class文件当前所在位置
  Last modified 2022-4-23; size 380 bytes //最后修改时间 文件大小
  MD5 checksum 87f79e3030c2bb49f83ec8b3d6db0294 //MD5值
  Compiled from "Test.java" //编译自哪个文件
public class Test //类的全限定名
  minor version: 0 //jdk次版本号
  major version: 52 //主版本号 ---> 1.8.0
  flags: ACC_PUBLIC, ACC_SUPER //该类的访问标志
Constant pool: //常量池
   #1 = Methodref          #6.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Fieldref           #5.#19         // Test.a:I
   #4 = Methodref          #20.#21        // java/io/PrintStream.println:(I)V
   #5 = Class              #22            // Test
   #6 = Class              #23            // java/lang/Object
   #7 = Utf8               a
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               f1
  #14 = Utf8               SourceFile
  #15 = Utf8               Test.java
  #16 = NameAndType        #9:#10         // "<init>":()V
  #17 = Class              #24            // java/lang/System
  #18 = NameAndType        #25:#26        // out:Ljava/io/PrintStream;
  #19 = NameAndType        #7:#8          // a:I
  #20 = Class              #27            // java/io/PrintStream
  #21 = NameAndType        #28:#29        // println:(I)V
  #22 = Utf8               Test
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/lang/System
  #25 = Utf8               out
  #26 = Utf8               Ljava/io/PrintStream;
  #27 = Utf8               java/io/PrintStream
  #28 = Utf8               println
  #29 = Utf8               (I)V
{
  private int a;
    descriptor: I
    flags: ACC_PRIVATE

  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void f1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #3                  // Field a:I
         7: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
}
SourceFile: "Test.java"
3.访问标志
标志名称含义
ACC_PUBLIC是否为Public类型
ACC_FINAL是否被声明为final,只有类可以设置
ACC_SUPER是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE标志这是一个接口
ACC_ABSTRACT是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC标志这个类并非由用户代码产生
ACC_ANNOTATION标志这是一个注解
ACC_ENUM标志这是一个枚举
4.常量池

​ 存放字面量(常量)和符号引用。

符号引用:

  1. 类和接口的全限定名
  2. 字段的名称和类型
  3. 方法的名称和返回值

eg. Main方法调用Object类的构造函数

#1 = Methodref          #6.#16         // java/lang/Object."<init>":()V
#6 = Class              #23            // java/lang/Object
#9 = Utf8               <init>
#10 = Utf8               ()V
#16 = NameAndType        #9:#10         // "<init>":()V
#23 = Utf8               java/lang/Object

eg. 类的属性a,类型为int

#3 = Fieldref           #5.#19         // Test.a:I
#5 = Class              #22            // Test
#7 = Utf8               a
#8 = Utf8               I
#19 = NameAndType        #7:#8          // a:I
#22 = Utf8               Test

类型表:

标识字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型long
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,以分号结尾,如Ljava/io/PrintStream;
5.方法表

​ 描述方法的类型、作用域

public void f1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #3                  // Field a:I
         7: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10

stack: 最大操作数栈

locals:局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。方法参数(包括实例方法中的隐藏参数this),显示异常处理器的参数(try catch中的catch块所定义的异常),方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是,locals的大小并不一定等于所有局部变量所占的Slot之和,因为局部变量中的Slot是可以重用的

args_size:this

attribute_info:方法体内容,输出int类型a属性的值,没有返回值

LineNumberTable:源码行号与字节码行号(字节码偏移量)之间的对应关系

6.字段表

​ 描述接口或类中声明的变量作用域、类型。

eg. Test类中a字段,int类型,private

  private int a;
    descriptor: I
    flags: ACC_PRIVATE
7.属性表

​ 描述字段表、方法表中特殊的属性

8.类索引、父类索引、接口索引

​ 用于确定类的继承关系

三、类的生命周期

在这里插入图片描述

1.加载

在这里插入图片描述

  • 通过类的全限定名得到二进制字节流

  • 将二进制字节流中静态代码块、静态方法转换为方法区运行时数据结构

  • 在堆中生成代表这个列的Class类模版

ClassLoader类加载器:

public class ClassLoader_ {

    public static void main(String[] args) {

        //获取应用程序类加载器
        ClassLoader classLoader1 = ClassLoader.getSystemClassLoader();
        //获取classLoader1的父类加载器
        ClassLoader classLoader2 = classLoader1.getParent();
        //获取classLoader2的父类加载器
        ClassLoader classLoader3 = classLoader2.getParent();

        System.out.println("应用程序类加载器:" +classLoader1);
        System.out.println("扩展类加载器:" + classLoader2);
        System.out.println("启动类加载器:" +classLoader3);

        //获取本类的class对象
        Class<ClassLoader_> aClass = ClassLoader_.class;

        ClassLoader classLoader = aClass.getClassLoader();

        System.out.println("本类的类加载器:" + classLoader);

        /*
            获取应用程序类能够加载的路径(类路径)
            D:\jdk\jre\lib\charsets.jar;
            D:\jdk\jre\lib\deploy.jar;
            D:\jdk\jre\lib\ext\access-bridge-64.jar;
            D:\jdk\jre\lib\ext\cldrdata.jar;
            D:\jdk\jre\lib\ext\dnsns.jar;
            D:\jdk\jre\lib\ext\jaccess.jar;
            D:\jdk\jre\lib\ext\jfxrt.jar;
            D:\jdk\jre\lib\ext\localedata.jar;
            D:\jdk\jre\lib\ext\nashorn.jar;
            D:\jdk\jre\lib\ext\sunec.jar;
            D:\jdk\jre\lib\ext\sunjce_provider.jar;
            D:\jdk\jre\lib\ext\sunmscapi.jar;
            D:\jdk\jre\lib\ext\sunpkcs11.jar;
            D:\jdk\jre\lib\ext\zipfs.jar;
            D:\jdk\jre\lib\javaws.jar;
            D:\jdk\jre\lib\jce.jar;
            D:\jdk\jre\lib\jfr.jar;
            D:\jdk\jre\lib\jfxswt.jar;
            D:\jdk\jre\lib\jsse.jar;
            D:\jdk\jre\lib\management-agent.jar;
            D:\jdk\jre\lib\plugin.jar;
            D:\jdk\jre\lib\resources.jar;
            D:\jdk\jre\lib\rt.jar;
            D:\java project\JVM\out\production\JVM;
            D:\java project\JVM\lib\junit-4.10.jar;
            D:\IntelliJ IDEA 2021.2.1\lib\idea_rt.jar
         */
        System.out.println(System.getProperty("java.class.path"));

        /*
            获取扩展类路径
            D:\jdk\jre\lib\ext;
            C:\Windows\Sun\Java\lib\ext
         */
        System.out.println(System.getProperty("java.ext.dirs"));
    }
}

/*
应用程序类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器:sun.misc.Launcher$ExtClassLoader@74a14482
启动类加载器:null
*/
  • 启动类加载器:

​ Bootstrap ClassLoader,jdk\jre\lib下、核心类库rt.jar,所有java.*开头的类都能加载。java程序无法获取到启动类加载器,它是由C/C++编写的

  • 扩展类加载器:

​ Extension ClassLoader,jdk\jre\lib\ext下、由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)

  • 应用程序累加载器:

    Application ClassLoader,加载用户类路径(ClassPath)所指定的类,程序默认的类加载器

注意:

1.当类加载器负责加载某个类时,该类中引用的其他类也交由该类加载器加载

2.加载过的类会被缓存,当加载某个类时,只有当缓存区不存在时,才会进行加载

3.以上三种类加载器只是从本地加载class文件,如果需要从其他地方加载class则必须自定义类加载器

2.验证

​ 确保加载类的正确

  • 文件格式验证:验证字节流是否为class文件,以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型
  • 元数据验证:进行语义的分析
  • 字节码验证:验证程序语义合法
  • 符号引用验证

(-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间)

3.准备

​ 为类的静态变量分配内存(在方法区中)并初始化默认值

​ 八大基本类型赋默认值(0,false),引用类型赋null值

eg. static int a = 1;

在准备阶段a的值为默认零值,在初始化阶段才会真正赋初始值1

4.解析

​ 将常量池中的符号引用转换为直接引用(真正的地址)

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量例如。在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

直接引用:直接指向目标的指针、相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)或一个间接定位到目标的句柄

5.初始化

​ 为类中的静态变量赋初始值

导致类初始化情况:

  1. new
  2. 访问类的静态变量或为其赋值
  3. 调用类的静态方法
  4. 反射获取类的Class类对象
  5. 初始化某个类的子类,其父类会被初始化
6.使用

访问方法区中的该类的字段方法,真正的数据位于堆中

7.卸载

JVM结束生命周期:

  1. System.exit()
  2. 正常执行结束
  3. 异常或错误终止

四、双亲委派机制

​ 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所有的类加载最终都应该被传递到顶层的启动类加载器中,只有当父加载器无法加载所需的类时,子加载器才会尝试自己去加载该类。

当AppClassLoader需要加载一个类时,它首先不会加载该类,而是委托给父类加载器ExtClassLoader,

而ExtClassLoader也不会加载该类,它也不会加载该类,委托给父类加载器BootstrapClassLoader,

当BootstrapClassLoader无法加载该类时,交给ExtClassLoader加载,

当ExtClassLoader无法加载该类时,交给AppClassLoader加载,

当AppClassLoader无法加载该类时,抛出ClassNotFoundException异常

双亲委派机制防止内存中出现和系统类相同的包名类名,保证java程序的安全

五、沙箱安全机制

​ 沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏

在这里插入图片描述

当前最新的安全机制实现,则引入了 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission),存在于不同域中的类文件就具有了当前域的全部权限。

六、PC寄存器

​ PC寄存器存储下一条指令的地址,即将要执行的指令代码,是线程私有的,生命周期和线程相同,每个线程都有属于自己的,是唯一不会导致OOM的地方。

​ 如果当前正在执行的是java方法,PC寄存器记录该指令地址,如果是本地方法(Native),则为undefine

七、虚拟机栈

  • 每创建一个线程就会创建一个虚拟机栈,栈中是一个个栈帧,对应着方法调用,是线程私有的,生命周期与线程相同

  • 栈不存在垃圾回收问题

  • 每个方法执行就会入栈,方法结束就会出栈(包括正常执行return返回、抛出异常)

  • 如果栈的大小固定,当线程请求分配栈容量超过最大容量会导致栈溢出(StackOverflowError),如果栈的大小可以动态增长,当分配的内存超出最大内存时会导致OOM(OutOfMemoryError),可以通过-Xss来设置线程的最大栈空间

在这里插入图片描述

一个线程在一个时刻只有一个活动的栈帧(栈顶栈帧),就是当前栈帧,即当前执行的方法

局部变量表:方法执行时完成参数传递,存储方法参数和定义在方法体内的局部变量(包括基本数据类型和对象引用),基本单位是Slot(槽),4个字节,当局部变量过了其作用域时该槽位可以重用节省资源

操作数栈:也称为表达式栈,保存计算过程的中间结果和变量

HotSpot栈顶缓存:将栈顶元素全部缓存到物理CPU寄存器中,降低对内存的读写,提高执行效率

动态链接:指向运行时常量池中该栈帧所属方法的引用,用于将符号引用转换为调用方法的直接引用

方法返回地址:存放调用该方法的 PC 寄存器的值,用于返回该方法被调用的位置,方法正常退出时,返回调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息,并且不会产生任何的返回值

附加信息:与 Java 虚拟机实现相关的一些信息

八、本地方法栈

本地方法接口:本地方法就是一个 Java 调用非 Java 代码的接口,例如Unsafe类中的本地方法

本地方法栈:用于管理登记本地方法,线程私有,在Hotspot中,本地方法栈和虚拟机栈合二为一

九、堆

在这里插入图片描述

为进行高效的垃圾回收,JVM把堆内存逻辑上分为三部分

年轻代(Young Generation):新对象和没达到一定年龄的对象,包括三个部分:伊甸园区、幸存者0区和幸存者1区,默认比例为8:1:1

老年代(Old Generation):被长时间使用的对象,老年代的内存空间应该要比年轻代更大

元空间:永久代(JDK8以前),JVM中方法区的实现

//JVM配置
//-Xms 设置初始化内存分配大小
//-Xmx 设置最大分配内存大小
//-XX:+PrintGCDetails 输出GC垃圾回收信息
//-XX:+HeapDump··· dump下程序的错误、异常 通过jprofiler工具定位具体的位置
public static void main(String[] args) {

    //获取JVM能使用的最大内存 默认为电脑内存的1/4
    long maxMemory = Runtime.getRuntime().maxMemory();

    //获取JVM初始化时总内存 默认为电脑内存的1/64
    long totalMemory = Runtime.getRuntime().totalMemory();

    //-Xms1g -Xmx1g -XX:+PrintGCDetails 设置JVM初始化时总内存、最大内存为1G并输出GC垃圾回收信息
    System.out.println("max = " + maxMemory + "字节, " + ((double)maxMemory / 1024 / 1024) + "M");
    System.out.println("total = " + totalMemory + "字节, " + ((double)totalMemory / 1024 / 1024) + "M");
}

/*
(本机内存8G)
----------默认-----------------
max = 1873805312字节, 1787.0M
total = 126877696字节, 121.0M
---------配置后----------------
max = 1029177344字节, 981.5M
total = 1029177344字节, 981.5M
Heap
 PSYoungGen      total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3166K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 344K, capacity 388K, committed 512K, reserved 1048576K
*/

默认情况下新生代和老年代的比例是 1:2,可以通过 –XX:NewRatio 来配置

新生代中的 伊甸园区、幸存者0区和幸存者1区,默认比例为8:1:1,可以通过 -XX:SurvivorRatio 配置

十、方法区

​ 所有线程共享的内存区域,也称为非堆(Non-Heap),存储存储类信息、常量池、静态变量、JIT编译后的代码等数据,只是一个概念,在Hotspot中实现方式为元空间

类型信息

对于每个加载的类型(类 class、接口 interface、枚举 enum、注解 annotation)存储

  1. 全限定名
  2. 直接父类全限定名
  3. 修饰符
  4. 直接接口列表

方法信息

对于每个方法存储:

  1. 方法名称
  2. 方法返回值类型
  3. 方法参数和类型
  4. 方法的修饰符
  5. 方法的字符码、操作数栈、局部变量表及大小(abstract 和 native 方法除外)
  6. 异常表(abstract 和 native 方法除外)

运行时常量池:JVM 为每个已加载的类型(类或接口)都维护一个动态的常量池

十一、JVM参数

JVM参数类型分类:

1.标配参数

-version 显示版本号

-help 帮助文档

java -showversion 显示版本号

2.X参数

-Xint 解释执行

-Xcomp 编译执行

-Xmixed 混合模式执行

在这里插入图片描述

3.XX参数

Boolean类型:

-XX:+/-属性值(+ 代表开启 -代表关闭)
jps -l //查看java 进程
jinfo -flag 属性名称 进程号 //查看java进程对应属性的值
jinfo -flags 进程号 //查看java进程所有属性值

Non-default为属性默认值,Command line为修改后的值

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

KV类型:

-XX:属性key=属性value

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

注意:

-Xms和-Xmx属于XX参数类型,它们只是别名,全称分别为:-XX:initialHeapSize和 -XX:MaxHeapSize

查看JVM参数默认值:

java -XX:+PrintFlagsInitial [-version]
java -XX:+PrintFlagsFinal [-version] //主要查看修改更新的值
java -XX:+PrintCommandLineFlags [-version] //查看命令行参数

在这里插入图片描述

<img src="D:\desktop\笔记\Java JVM.assets\image-在这里插入图片描述

(在参数列表中,=表示默认参数值,:=表示修改过的参数值)

在这里插入图片描述

常用参数表:

参数名解释
-Xms初始化堆内存(-XX:InitialHeapSize 默认为物理内存的1/64)
-Xmx最大分配内存(-XX:MaxHeapSize 默认为物理内存的1/4)
-Xss单个栈的大小(-XX:ThreadStack 默认在512k~1024k)
-Xmn新生代大小
-XX:MetaspaceSize元空间大小(默认20M左右 可以设置大一些避免OOM)
-XX:+PrintGCDetails开启GC日志
-XX:SurvivorRatio新生代中伊甸园区、幸存者From区和幸存者To区所占空间的比例(默认为8,即8:1:1)
-XX:NewRatio新生代和老年代比例(默认为2,即老年代占2新生代占1)
XX:MaxTenuringThreshold从新生代进入老年代经历gc的次数(默认为15)

十二、JIT

JIT(Just-In-Time Compiler)是即时编译器,是一种提高程序运行效率的方法。通常程序有两种运行方式:

  • 静态编译执行:在执行前全部被翻译为机器码,运行效率高很多,但有额外开销
  • 动态解释执行:一句一句边运行边翻译,没有编译时间,性能相对差一些

二八定律:20%的热点代码占据了程序80%的执行时间

热点代码有两类:

  • 被多次执行的方法
  • 多次执行的循环体

一般的代码解释执行,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会认为这些代码是热点代码

为了提高热点代码的效率,会用JIT 把这些代码编译成机器码,并进行各层次优化。

hotspot中采用热点探测找到热点代码

方法调用计数器(invacation counter):用于统计方法被调用的次数

回边计数器(back edge Counter):用于统计一个方法中循环代码的执行次数,在字节码中遇到控制流向后跳转的指令称为回边,回边计数器统计的目的就是为了触发OSR(On Stack Replacement)栈上替换编译

在这里插入图片描述

在这里插入图片描述

十三、逃逸分析和代码优化

分析对象动态作用域:

  1. 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  2. 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸

栈上分配:如果一个对象的指针永远不会逃逸,将堆分配转化为栈分配

同步省略:也叫锁消除,如果一个对象只被一个线程访问,则对其的操作可以不考虑同步

public class SynchronizedClear_ {

    public void test(){
        Object o = new Object();
	    
        //给test()方法中的局部变量上锁,等同于未加锁
        synchronized (o){
            System.out.println("test....");
        }
    }
}

标量替换:如果一个对象不会被外部访问,并且该对象可以被进一步分解时,JVM 不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替

缺点:逃逸分析相对耗时,无法保证逃逸分析的性能消耗一定能高于他的消耗

十四、垃圾回收(GC)

​ 垃圾收集主要是针对堆和方法区进行,程序计数器、虚拟机栈和本地方法栈是线程私有的,线程执行完毕就会销毁,无需进行垃圾回收

1.方法区上的GC

​ 方法区主要存放永久代对象回收率低,只要是对常量池的回收和对类的卸载

类卸载的条件:

  • 类的所有实例都被回收,堆中不存在该类的实例

  • 加载该类的ClassLoader被回收

  • 类的Class类对象没有在其他地方引用

2.堆上的GC

Minor GC/Young GC:轻GC,发生在新生代,会频繁执行,速度很快

Major GC/Old GC:单独回收老年代的垃圾(CMS中使用)

Full GC:回收整个java堆和方法区的垃圾

引用计数法(JVM不使用):

在这里插入图片描述

​ 给每个对象设置一个计数器,当对象增加一个引用时计数器加一,引用失效计数器减一,只回收计数器你为0的对象。如果存在循环引用的情况,引用计数器不可能为0,无法对它们进行回收

为解决循环引用问题,使用可达性分析,通过GC Roots作为起始点,如果一个对象到GC Roots没有任何链

路可用则说明对象不可用会被视为垃圾

在这里插入图片描述

GC Roots是一个set集合,包括:

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

复制算法(新生代)

在这里插入图片描述

​ 对于新生代区域,每次将Eden(伊甸园区)和From幸存者区中存活的对象复制到To幸存区之中,复制完成后,幸存区名字交换,在两个幸存区中始终保证有一个为空。占用内存空间,有一半的空间浪

标记清除算法(老年代)

在这里插入图片描述

​ 对于老年代区域,对存活的对象进行标记,每次清除掉未标记的对象。但是标记影响效率,清除之后会产生大量不连续的内存碎片

标记清除整理算法(老年代)

在这里插入图片描述

​ 在标记清除算法基础之上,将存活的对象移到一端,避免产生内存碎片。但是移动存活对象也影响效率

分代收集算法:

​ 对于堆中不同区域采取不同的方式:

  • 新生代使用复制算法
  • 老年代使用标记清除整理算法
3.垃圾回收器

jdk8垃圾回收器主要有6种:

  • SerialGC
  • ParallelGC
  • ConcMarkSweepGC
  • ParNewGC
  • ParallelNewGC(新生代并行)
  • ParallelOldGC(老年代并行)
  • G1GC

在这里插入图片描述

1.SerialGC

只使用单个线程进行垃圾回收,会暂停用户线程,不适合服务器

在这里插入图片描述

-XX:+UseSerialGC

使用SerialGC(复制) + SerialOldGC(标记-整理)

在这里插入图片描述

在这里插入图片描述

DefNew:Default New Generation

Tenured:Old

2.ParNewGC

在这里插入图片描述

是SerialGC新生代并行版本,多个线程共同工作,会暂停用户线程,适用于科学计算/大数据等弱交互场景

-XX:+UseParallelNewGC

使用ParNew(复制) + Serial Old(标记-整理)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

ParNew:Parallel New Generation

Serial Old垃圾收集器已经过时被弃用

3.ParallelGC

在新生代和老年代都是用并行回收

在这里插入图片描述

-XX:+UseParallelGC-XX:UseParallelOldGC //可相互激活

在这里插入图片描述

在这里插入图片描述

PSYoungGen:Parallel Scavenge

ParOldGen:Parallel Old Generation

自适应调节

吞吐量即用户代码运行时间 / (用户代码运行时间 + 垃圾收集时间),-XX:MaxGCPauseMills设置停顿时间,

-XX:ParallelGCThreads设置GC并发线程数,cpu > 8时设置为 5 / 8,cpu < 8时为实际个数

4.CMSGC

用户线程和垃圾回收线程可以同时执行,用户线程停顿低,适用于对响应时间有要求的场景

在这里插入图片描述

步骤:

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

优点:并发收集响应快

缺点:并发执行cpu压力大,会产生大量内存碎片

-XX:+UseConcMarkSweepGC //会激活-XX:+UseParNewGC

使用ParNew(复制) + CMS(并发标记清除) + Serial Old(作为CMS出错的后备收集器)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
三色标记算法:

黑色:GCRoots不会是垃圾标记为黑色,如果扫描对象和它所直接引用的所有对象都被访问过则被标记为黑色

灰色:如果扫描对象的直接引用至少有一个没有被访问则被标价为灰色

白色:如果对象没有被访问则被标记为白色

在这里插入图片描述

  • 先将GCRoots标记为黑色,它的所有直接引用标记成灰色
  • 然后依次访问灰色对象,将其标记为黑色,将它的直接引用标记为灰色
  • 所有标记完成后,白色对象称为垃圾被清理掉

由于CMS的并发标记过程不会暂停用户线程,用户线程可能修改对象引用漏标和错标的问题

  • 漏标:由于用户线程修改引用(置null),被当做垃圾,但在此次gc中已经被标记为黑色无法删除,这种垃圾被称为浮动垃圾,可以不进行处理在下一次gc中删除
  • 错标:由于用户线程引用了白色对象(垃圾),但在此次gc中对白色对象进行删除,导致代码报错

针对错标问题,使用增量更新和原始快照解决

  • 增量更新:当黑色对象新增一个白色对象引用之前添加一个写屏障,将新增的引用记录下来,在重新标记阶段,再以这些引用关系中的黑色对象为根,再扫描一次
  • 原始快照(SATB):在用户线程对引用赋值(置空)之前添加一个写屏障,直接把它标记为黑色,默认它不是垃圾不需要清理

原始快照比增量更新效率高,因为不用在重新标记阶段再去遍历,但是也就可能会导致有更多的浮动垃圾

G1使用的就是原始快照,CMS使用的是增量更新

5.G1

不在按照新生代、老年代分别回收,将堆内存分割成不同的**区域(Region)**然后并发的进行垃圾回收

在这里插入图片描述

每个区域会在新生代、老年代之间切换,标记整理之后使得空闲区域连接起来没有内存碎片

在这里插入图片描述

步骤:

  1. 初始标记:只标记GC Roots能直接关联到的对象
  2. 并发标记:进行可达性分析过程
  3. 最终标记:修正并发标记期间,因程序运行导致标记发生改变的对象
  4. 筛选回收:根据时间来进行价值最大化的回收

优点:

G1回收器没有内存碎片,可以控制GC完成的时间

在这里插入图片描述

在这里插入图片描述

G1常用参数:

参数名解释
-XX:G1HeapRegionSize设置G1区域大小(必须为2的幂次方,在1MB~32MB)
-XX:MaxGCPauseMills最大GC停顿时间(GC完成时间)
-XX:InitiatingHeapOccupancyPercent堆内存占用多少时就触发GC(默认为45%)
-XX:ConcGCThreads并发GC使用的线程数
-XX:G1ReservePercent设置作为空闲空间的预留内存百分比,避免空间溢出(默认为10%)

垃圾回收器的选择

  • 单cpu或小内存使用 -XX:+UseSerialGC
  • 多cpu吞吐量高 -XX:UseParallelGC 或者 -XX:+UseParallelOldGC
  • 多cpu需要快速响应 -XX:UseConcMarkSweepGC

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值