StringTable/String Pool

字符串常量池(String Pool / StringTable / 串池)存储的是 String 对象的直接引用或者对象,即保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定,字符串常量池类似于 Java 系统级别提供的缓存,存放对象和引用

StringTable,类似 HashTable 结构,通过 -XX:StringTableSize 设置大小,JDK 1.8 中默认 60013

字符串拼接
// 字符串常量池(StringTable): [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public static void main(String[] args) {
  String s1 = "a"; // 懒惰的
  String s2 = "b";
  String s3 = "ab";
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

字节码:Java 反编译指令javap -v 文件名.class

//常量池
// 常量池中的信息,都会被加载到运行时常量池中, 
// 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象,是懒惰的
Constant pool: 
   #1 = Methodref          #12.#36        // java/lang/Object."":()V
   #2 = String             #37            // a
   #3 = String             #38            // b
   #4 = String             #39            // ab


//运行代码       
 0: ldc           #2                  // String a
 2: astore_1 //存入局部变量表slot 1号位
 3: ldc           #3                  // String b
 5: astore_2
 6: ldc           #4                  // String ab
 8: astore_3
   
// ldc #2 会把 a 符号变为 "a" 字符串对象,StringTable: ["a"] 
// ldc #3 会把 b 符号变为 "b" 字符串对象,StringTable: ["a", "b"] 
// ldc #4 会把 ab 符号变为 "ab" 字符串对象,StringTable: ["a", "b" ,"ab"]    
   
       
//局部变量表(栈)  
LocalVariableTable:  
        Start  Length  Slot  Name   Signature
            0      51     0  args   [Ljava/lang/String;
            3      48     1    s1   Ljava/lang/String;
            6      45     2    s2   Ljava/lang/String;
            9      42     3    s3   Ljava/lang/String;
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
// 字符串常量池(StringTable): [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public static void main(String[] args) {
  String s1 = "a"; // 懒惰的
  String s2 = "b";
  String s3 = "ab";
  // new StringBuilder().append("a").append("b").toString() -->  new String("ab")  堆中
  String s4 = s1 + s2;   //字符串变量   // 返回的是堆内地址
  // javac 在编译期间的优化,结果已经在编译期确定为ab
  String s5 = "a" + "b"; //字符串常量
  
  System.out.println(s3 == s4); // F
  System.out.println(s3 == s5); // T
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
intern()

JDK 1.8:将这个字符串对象尝试放入串池,如果 String Pool 中:

JDK 1.6:将这个字符串对象尝试放入串池,如果 String Pool 中:

// StringTable: ["ab", "a", "b"]
public static void main(String[] args) {

  String x = "ab"; // StringTable: ["ab"]
  
  // 堆  new String("a")   new String("b")  new StringBuilder()  new String("ab")
  // StringTable: ["ab", "a", "b"]
  String s = new String("a") + new String("b");   // s只存在堆中,不存在StringTable

  // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
  String s2 = s.intern(); 

  System.out.println(s == x);  // F
  System.out.println(s2 == x); // T
  
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

结论:

String s1 = "ab";								// ab仅放入串池  StringTable: ["ab"]

String s2 = new String("a") + new String("b");	// ab仅放入堆  StringTable: ["a","b"]

String s = new String("ab"); // ab串池和堆都存在 StringTable: ["ab"]
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

常见问题

问题一:

public static void main(String[] args) {
    String s = new String("a") + new String("b");//new String("ab")
    //在上一行代码执行完以后,字符串常量池中并没有"ab"

    String s2 = s.intern();
    //jdk6:串池中创建一个字符串"ab",把此对象复制一份
    //jdk8:串池中没有创建字符串"ab",而是创建一个引用指向 new String("ab"),将此引用返回

    System.out.println(s2 == "ab");//jdk6:true  jdk8:true
    System.out.println(s == "ab");//jdk6:false  jdk8:true
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

问题二:

public static void main(String[] args) {
    String str1 = new StringBuilder("58").append("tongcheng").toString();
    System.out.println(str1 == str1.intern());//true,字符串池中不存在,把堆中的引用复制一份放入串池

    String str2 = new StringBuilder("ja").append("va").toString();
    System.out.println(str2 == str2.intern());//false,字符串池中存在,直接返回已经存在的引用
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

原因:

内存位置

Java 7 之前,String Pool 被放在运行时常量池中,属于永久代;Java 7 以后,String Pool 被移到堆中,这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误

演示 StringTable 位置:

img


优化常量池

两种方式:

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {
    public static void main(String[] args) throws IOException {
        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            //很多数据
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
不可变好处

5.方法区

方法区 Method Area:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)

Java 1.8 以前:方法区由永久代实现

Java 1.8 之后:方法区由元空间实现

异常:java.lang.OutOfMemoryError:Metaspace

img

方法区构成:

特点:

❷本地内存

img

JVM内存:Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报 OOM

本地内存:又叫做堆外内存,线程共享的区域,本地内存这块区域是不会受到 JVM 的控制的,不会发生 GC;因此对于整个 Java 的执行效率是提升非常大,但是如果内存的占用超出物理内存的大小,同样也会报 OOM

1.方法区/元空间

Java8 开始 PermGen 被元空间代替,永久代的类信息、方法、常量池等都移动到元空间区

元空间与永久代区别:元空间不在虚拟机中,使用的本地内存,默认情况下,元空间的大小仅受本地内存限制

方法区内存溢出:

元空间内存溢出演示:

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

2.直接内存

直接内存是 Java 堆外、直接向系统申请的内存区间,不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

概述

Direct Memory 优点:

直接内存缺点:

应用场景:

数据流的角度:

class="table-box">
直接内存img
非直 接内存img
ByteBuffer

img

ByteBuffer 有两种类型:

class="table-box">
描述优点
HeapByteBuffer在jvm堆上面的一个buffer,底层的本质是一个数组由于内容维护在jvm里,所以把内容写进buffer里速度会快些;并且,可以更容易回收
DirectByteBuffer底层的数据其实是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据跟外设(IO设备)打交道时会快很多,因为外设读取jvm堆里的数据时,不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的,如果使用DirectByteBuffer,则可以省去这一步,实现zero copy
分配回收

直接内存 DirectByteBuffer 源码分析:

DirectByteBuffer(int cap) { 
    //....
    long base = 0;
    try {
        // 分配直接内存
        base = unsafe.allocateMemory(size);
    }
    // 内存赋值
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 创建回收函数
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}
private static class Deallocator implements Runnable {
    public void run() {
        unsafe.freeMemory(address);// 释放内存
        //...
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

分配和回收原理

❸JVM运行原理

img

接下来,我们通过一个案例来了解下代码和对象是如何分配存储的,Java 代码又是如何在 JVM 中运行的。

public class JVMCase {

  // 常量
  public final static String MAN_SEX_TYPE = "man";
  // 静态变量
  public static String WOMAN_SEX_TYPE = "woman";

  // 静态方法
  public static void print(Student stu) {
    System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge()); 
  }
  // 非静态方法
  public void sayHello(Student stu) {
    System.out.println(stu.getName() + "say: hello"); 
  }
  
  public static void main(String[] args) {
    Student stu = new Student();
    stu.setName("nick");
    stu.setSexType(MAN_SEX_TYPE);
    stu.setAge(20);
    
    JVMCase jvmcase = new JVMCase();
    // 调用静态方法
    print(stu);
    // 调用非静态方法
    jvmcase.sayHello(stu);
  }

}

@Data
class Student{
  String name;
  String sexType;
  int age;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

运行上面代码时,JVM的整个处理过程如下:

img

img

❹总结

1.异常

常见 Out Of Memory(OOM) 错误:

Java 编译指令:javac -g 文件名.java -g 可以生成所有相关信息

Java 反编译指令:javap -v 文件名.class -v 输出附加信息

后台运行:nohup java 全路径名

2.三种常量池

常量池:主要存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References)

运行时常量池:运行时常量池里面存储的主要是编译期间生成的字面量、符号引用等等。

字符串常量池:可以理解成运行时常量池分出来的一部分。类加载到内存的时候,字符串会存到字符串常量池里面。

3者区别?

在类编译过程中,会把类元信息放到方法区,类元信息的其中一部分便是常量池,主要存放字面量和符号引用,而字面量的一部分便是文本字符;在类加载时将字面量和符号引用解析为直接引用存储在运行时常量池;对于文本字符,会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池

3.变量位置

变量的位置不取决于它是基本数据类型还是引用数据类型,取决于它的声明位置

静态内部类和其他内部类:方法区/堆

类变量:堆

实例变量:堆

局部变量:虚拟机栈

③对象实例化

❶对象内存结构

一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)

对象头:

|--------------------------------------------------------------|
|                         Object Header (64 bits)              |
|------------------------------------|-------------------------|
|           Mark Word (32 bits)      |    Klass Word (32 bits) |
|------------------------------------|-------------------------|
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志(最后两位)、线程持有的锁、偏向线程ID、偏向时间戳等等

32 位虚拟机 Mark Word

|-------------------------------------------------------|--------------------|
|                    Mark Word (32 bits)                |        State       |
|-------------------------------------------------------|--------------------|
|              hashcode:25 | age:4 | biased_lock:0 | 01 |        Normal      |
|-------------------------------------------------------|--------------------|
|      thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 |        Biased      |
|-------------------------------------------------------|--------------------|
|                            ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|                    ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                                  | 11 |    Marked for GC   |
|-------------------------------------------------------|--------------------|
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

64 位虚拟机 Mark Word

|--------------------------------------------------------------------|--------------------|
|                      Mark Word (64 bits)                           |       State        |
|--------------------------------------------------------------------|--------------------|
|    unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 |       Normal       |
|--------------------------------------------------------------------|--------------------|
|        thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 |       Biased       |
|--------------------------------------------------------------------|--------------------|
|                                         ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
|                                 ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
|                                                               | 11 |    Marked for GC   |
|--------------------------------------------------------------------|--------------------|
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
hash(25) + age(4) + lock(3) = 32bit					#32位系统
unused(25+1) + hash(31) + age(4) + lock(3) = 64bit	#64位系统
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

Klass Word:类型指针,指向该对象的 Class 类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,默认开启指针压缩(-XX:+UseCompressedOops),使用32bits指针。堆内存大于32G时,压缩指针会失效,会强制使用64bits来进行对象寻址

|-------------------------------------------------------------------------------|
| 						              Object Header (96 bits) 							              |
|-----------------------|-----------------------------|-------------------------|
|  Mark Word(32bits)    | 	  Klass Word(32bits) 	    |   array length(32bits)  |
|-----------------------|-----------------------------|-------------------------|
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来

对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全

32 位系统:

img

❷对象访问方式

JVM 是通过**栈帧中的对象引用(reference)**访问到堆中的对象实例:

❸对象创建过程

创建对象的方式

创建对象的过程

  1. 类加载检查

当虚拟机遇到一条 new 指令时,首先检查是否能在运行时常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那先执行类加载。

  1. 为对象分配内存

首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。

选择哪种分配方式由堆是否规整所决定,而堆是否规整又由所采用的GC收集器是否带有压缩整理功能决定。

内存分配并发问题

在创建对象的时候有一个很重要的问题,就是线程安全,虚拟机采用两种方式来保证线程安全:

  1. 初始化零值

分配到的内存空间都初始化为零值,通过这个操作保证了对象的字段可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  1. 设置对象头

将对象的所属类(即类的元数据信息)、对象的哈希码、对象的GC分代年龄、锁信息等数据存储在对象的对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  1. 执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,执行 new 指令之后会接着执行 `` 方法(初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量),把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

③垃圾回收

❶内存分配

1.两种方式

JVM 为对象分配内存的过程:首先计算对象占用空间大小,接着在堆中划分一块内存给新对象

2.TLAB

Thread Local Allocation Buffer,TLAB 是虚拟机在堆内存的 Eden 划分出来的一块专用空间,是线程专属的。

在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

多线程分配内存时,使用 TLAB 可以避免线程安全问题,同时还能够提升内存分配的吞吐量,这种内存分配方式叫做快速分配策略

我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的。

堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据,由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度

问题:堆空间都是共享的么? 不一定,因为还有 TLAB,在堆中划分出一块区域,为每个线程所独占

img

JVM 是将 TLAB 作为内存分配的首选,但不是所有的对象实例都能够在 TLAB 中成功分配内存,一旦对象在 TLAB 空间分配内存失败时,JVM 就会通过使用加锁机制确保数据操作的原子性,从而直接在堆中分配内存

栈上分配优先于 TLAB 分配进行,逃逸分析中若可进行栈上分配优化,会优先进行对象栈上直接分配内存

参数设置:

img

❷分代思想

1.分代介绍

Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代

Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。 Eden 和 Survivor 大小比例默认为 8:1:1

Old 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Old 区

img

分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能

GC


2.分代分配

工作机制:

晋升到老年代:

空间分配担保:

❸回收策略

1.触发条件

内存垃圾回收机制主要集中的区域就是线程共享区域:堆和方法区

Minor GC 触发条件:当 Eden 空间满时,就将触发一次 Minor GC

Full GC 同时回收新生代、老年代和方法区,有以下触发条件:

手动 GC 测试,VM参数:-XX:+PrintGcDetails

public void localvarGC1() {
    byte[] buffer = new byte[10 * 1024 * 1024];//10MB
    System.gc();	//输出: 不会被回收, FullGC时被放入老年代
}

public void localvarGC2() {
    byte[] buffer = new byte[10 * 1024 * 1024];
    buffer = null;
    System.gc();	//输出: 正常被回收
}
 public void localvarGC3() {
     {
         byte[] buffer = new byte[10 * 1024 * 1024];
     }
     System.gc();	//输出: 不会被回收, FullGC时被放入老年代
 }

public void localvarGC4() {
    {
        byte[] buffer = new byte[10 * 1024 * 1024];
    }
    int value = 10;
    System.gc();	//输出: 正常被回收,slot复用,局部变量过了其作用域 buffer置空
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

2.安全区域

安全点 (Safepoint):程序执行时并非在所有地方都能停顿下来开始 GC,只有在安全点才能停下

在 GC 发生时,让所有线程都在最近的安全点停顿下来的方法:

问题:Safepoint 保证程序执行时,在不太长的时间内就会遇到可进入 GC 的 Safepoint,但是当线程处于 Waiting 状态或 Blocked 状态,线程无法响应 JVM 的中断请求,运行到安全点去中断挂起,JVM 也不可能等待线程被唤醒,对于这种情况,需要安全区域来解决

安全区域 (Safe Region):指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的

运行流程:

3.GC分类

❹垃圾判断

1.垃圾介绍

垃圾:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾

作用:释放没用的对象,清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象

区域:垃圾收集主要是针对方法区进行,程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收

在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象,GC 才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程可以称为垃圾标记阶段,判断对象存活一般有两种方式:引用计数算法可达性分析算法

2.引用计数法

引用计数算法(Reference Counting):对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;当引用失效时,引用计数器就减 1;当对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收(Java 没有采用)

优点:

缺点:

3.可达性分析

可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集

GC Roots :GC Roots 是一组活跃的引用,不是对象,放在 GC Roots Set 集合

工作原理

可达性分析算法以 GC Roots 为起始点,从上至下的方式搜索被 GC Roots 所连接的目标对象

分析工作必须在一个保障一致性的快照中进行,否则结果的准确性无法保证,这也是导致 GC 进行时必须 Stop The World 的一个原因

4.引用分析

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

5.三色标记

基本算法

三色标记法把遍历对象图过程中遇到的对象,标记成以下三种颜色:

当 Stop The World (STW) 时,对象间的引用是不会发生变化的,可以轻松完成标记,遍历访问过程为:

  1. 初始时,所有对象都在 【白色集合】中;
  2. 将 GC Roots 直接引用到的对象挪到 【灰色集合】中;
  3. 从灰色集合中获取对象:
  4. 重复步骤3,直至【灰色集合】为空时结束。
  5. 结束后,仍在【白色集合】的对象即为 GC Roots 不可达,可以进行回收。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

三色标记遍历过程

并发标记时,对象间的引用可能发生变化,多标和漏标的情况就有可能发生

多标-浮动垃圾

多标情况:当 E 变为灰色时,断开 D 对 E 的引用,导致对象 E/F/G 仍会被标记为存活,本轮 GC 不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾

img

漏标-读写屏障

漏标情况:当 E 变为灰色时,断开 E 对 G 的引用,再让 D 引用 G。此时切回 GC 线程继续跑,因为 E 已经没有对 G 的引用了,所以不会将 G 放到灰色集合;尽管 D 重新引用了G,但 D 已经是黑色了,不会再重新做遍历处理。
最终导致的结果是:G 会一直停留在白色集合中,最后被当作垃圾进行清除。这直接
影响到了应用程序的正确性
,是不可接受的。

img

即漏标只有同时满足以下两个条件时才会发生:

代码角度解释漏标:

var G = objE.fieldG; // 1.读
objE.fieldG = null;  // 2.写
objD.fieldG = G;     // 3.写
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
  1. 读取 对象E的成员变量fieldG的引用值,即对象G;
  2. 对象E 往其成员变量fieldG,写入 null值。
  3. 对象D 往其成员变量fieldG,写入 对象G ;

为了解决问题,我们只要在上面这三步中的任意一步中做一些“手脚”,将对象G记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的GC Roots遍历完(并发标记),再遍历该集合(重新标记)。

重新标记通常是需要STW的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。当然,并发标记期间也可以将该集合中的大部分先跑了,从而缩短重新标记STW的时间,这个是优化问题了。

解决方法:添加读写屏障,读屏障拦截第一步,写屏障拦截第二三步,在读写前后进行一些后置处理:

以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下:

6.finalization

Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑

垃圾回收对象之前,会先调用这个对象的 finalize() 方法,finalize() 方法允许在子类中被重写,用于在对象被回收时进行后置处理,通常在这个方法中进行一些资源释放和清理,比如关闭文件、套接字和数据库连接等

生存 OR 死亡:如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用,此对象需要被回收。但事实上这时候它们暂时处于缓刑阶段。一个无法触及的对象有可能在某个条件下复活自己,所以虚拟机中的对象可能的三种状态:

永远不要主动调用某个对象的 finalize() 方法,应该交给垃圾回收机制调用,原因:

7.无用属性

无用类

方法区主要回收的是无用的类

判定一个类是否是无用的类,需要同时满足下面 3 个条件:

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是可以,而并不是和对象一样不使用了就会必然被回收

废弃常量

在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该常量,说明常量 “abc” 是废弃常量,如果这时发生内存回收的话而且有必要的话(内存不够用),”abc” 就会被系统清理出常量池

静态变量

类加载时(第一次访问),这个类中所有静态成员就会被加载到静态变量区,该区域的成员一旦创建,直到程序退出才会被回收

如果是静态引用类型的变量,静态变量区只存储一份对象的引用地址,真正的对象在堆内,如果要回收该对象可以设置引用为 null

❺回收算法

1.标记复制

复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清理,交换两个内存的角色,完成垃圾的回收

img

算法优点:

算法缺点:

基于新生代 “朝生夕灭” 的特点,大多数虚拟机都不会按照 1:1 的比例来进行内存划分,例如 HotSpot 虚拟机会将内存空间划分为一块较大的 Eden 和 两块较小的 Survivor 空间,它们之间的比例是 8:1:1 。 每次分配时只会使用 Eden 和其中的一块 Survivor ,发生垃圾回收时,只需要将存活的对象一次性复制到另外一块 Survivor 上,这样只有 10% 的内存空间会被浪费掉。当 Survivor 空间不足以容纳一次 Minor GC 时,此时由其他内存区域(通常是老年代)来进行分配担保。

应用场景:如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之则不适合

现在的商业虚拟机都采用这种收集算法回收新生代,因为新生代 GC 频繁并且对象的存活率不高,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间

2.标记清除

标记清除算法,是将垃圾回收分为两个阶段,分别是标记和清除

img

算法缺点:

算法优点:

3.标记整理

标记整理(压缩)算法是在标记清除算法的基础之上,做了优化改进的算法

img

优点:不会产生内存碎片

缺点:需要移动大量对象,处理效率比较低

4.对比总结

class="table-box">
算法速度空间开销移动对象
复制算法最快通常需要活对象的 2 倍大小(不堆积碎片)
标记清除中等少(但会堆积碎片)
标记整理最慢少(不堆积碎片)

❻垃圾回收器

0.概述

a.垃圾收集器分类
b.GC 性能指标
c.垃圾收集器的组合关系

img

红色虚线在 JDK9 移除、绿色虚线在 JDK14 弃用该组合、青色虚线在 JDK14 删除 CMS 垃圾回收器

Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 GC 不同:

img

查看默认的垃圾收回收器:

1.Serial/Serial old

Serial:串行垃圾收集器,作用于新生代,使用单线程进行垃圾回收,采用复制算法,新生代基本都是复制算法

STW(Stop-The-World):垃圾回收时,只有一个线程在工作,并且 Java 应用中的所有线程都要暂停,等待垃圾回收的完成

Serial old:执行老年代垃圾回收的串行收集器,内存回收算法使用的是标记-整理算法,同样也采用了串行回收和 STW 机制

开启参数:-XX:+UseSerialGC 等价于新生代用 Serial GC 且老年代用 Serial old GC

img

优点:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,可以获得最高的单线程收集效率

缺点:对于交互性较强的应用而言,这种垃圾收集器是不能够接受的,比如 JavaWeb 应用

2.ParNew

Par 是 Parallel 并行的缩写,New 是只能处理的是新生代

并行垃圾收集器在串行垃圾收集器的基础之上做了改进,采用复制算法,将单线程改为了多线程进行垃圾回收,可以缩短垃圾回收的时间

对于其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样,应用在年轻代,除 Serial 外,只有ParNew GC 能与 CMS 收集器配合工作

相关参数:

img

ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器

3.Parallel/Parallel Old

Parallel Scavenge 收集器:是应用于新生代的并行垃圾回收器,采用复制算法、并行回收和 Stop the World 机制

Parallel Old :是应用于老年代的并行垃圾回收器,采用标记-整理算法

对比其他回收器:

应用场景:

停顿时间和吞吐量的关系:新生代空间变小 → 缩短停顿时间 → 垃圾回收变得频繁 → 导致吞吐量下降

在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器,在 Server 模式下的内存回收性能很好,Java8 默认是此垃圾收集器组合

img

参数配置:

4.CMS

Concurrent Mark Sweep(CMS),是一款并发的、使用标记-清除算法、响应时间优先针对老年代的垃圾回收器,其最大特点是让垃圾收集线程与用户线程同时工作

CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短(低延迟)越适合与用户交互的程序,良好的响应速度能提升用户体验

分为以下四个流程:

img

Mark Sweep 会造成内存碎片,不把算法换成 Mark Compact 的原因:Mark Compact 算法会整理内存,导致用户线程使用的对象的地址改变,影响用户线程继续执行

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

优点:并发收集、低延迟

缺点:

参数设置:

5.G1

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,应用于新生代和老年代、采用标记-整理算法、响应时间优先、软实时、低延迟、可设定目标(最大 STW 停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1

JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器

应用场景:

G1 优点

Region 结构图

Region 结构图

G1 缺点
记忆集-RSet

为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进 GC Roots 扫描范围。

记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来记录被哪些其他 Region 里的对象引用(谁引用了我就记录谁)

img

通过写屏障来更新记忆集:

程序对 Reference 类型数据写操作时,产生一个写屏障 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中,进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏

卡表-Card Table

垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式:

卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等

卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,在垃圾收集发生时,只要筛选出卡表中 dirty 的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。JVM 对于卡页的维护也是通过写屏障的方式

img

写屏障-Write Barrier

我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题,例如它们何时变脏、谁来把它们变脏等。

卡表元素何时变脏

有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。

如何变脏,即如何在对象赋值的那一刻去更新维护卡表呢?

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。

写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。

回收集-CSet

Collection Set 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中

CSet根据两种不同的回收类型分为两种不同CSet。

工作原理

G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 Full GC,在不同的条件下被触发

img

顺时针:Minor GC → Minor GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收

相关参数
调优

G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优:

  1. 开启 G1 垃圾收集器
  2. 设置堆的最大内存
  3. 设置最大的停顿时间(STW)

不断调优暂停时间指标:

不要设置新生代和老年代的大小:

6.ZGC

ZGC 收集器是JDK 11中推出的一个可伸缩的、低延迟的垃圾收集器,基于 Region 内存布局的,不设分代,使用了读屏障染色指针内存多重映射等技术来实现可并发的标记-整理算法

ZGC 目标:

ZGC 的工作过程可以分为 4 个阶段:

ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的,但这部分的实际时间是非常少的,所以响应速度快,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟

优点:高吞吐量、低延迟

缺点:浮动垃圾,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾

对比:

G1 需要通过写屏障来维护记忆集,才能处理跨代指针,得以实现Region的增量回收。记忆集要占用大量的内存空间,写屏障也对正常程序运行造成额外负担,这些都是权衡选择的代价。

ZGC就完全没有使用记忆集,它甚至连分代都没有,连像CMS中那样只记录新生代和老年代间引用的卡表也不需要,因而完全没有用到写屏障,所以给用户线程带来的运行负担也要小得多。可是,当 ZGC 准备要对一个很大的堆做一次完整的并发收集,其全过程要持续十分钟以上,由于应用的对象分配速率很高,将创造大量的新对象产生浮动垃圾

新一代垃圾回收器ZGC的探索与实践 - 美团技术团队 (meituan.com)

对比总结

img

jdk8环境下,默认使用 Parallel Scavenge + Parallel Old

④类加载

❶类文件结构

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类可以有多个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
class="table-box">
类型名称说明长度数量
u4magic魔数,识别类文件格式4个字节1
u2minor_version副版本号(小版本)2个字节1
u2major_version主版本号(大版本)2个字节1
u2constant_pool_count常量池计数器2个字节1
cp_infoconstant_pool常量池表n个字节constant_pool_count-1
u2access_flags访问标识2个字节1
u2this_class类索引2个字节1
u2super_class父类索引2个字节1
u2interfaces_count接口计数2个字节1
u2interfaces接口索引集合2个字节interfaces_count
u2fields_count字段计数器2个字节1
field_infofields字段表n个字节fields_count
u2methods_count方法计数器2个字节1
method_infomethods方法表n个字节methods_count
u2attributes_count属性计数器2个字节1
attribute_infoattributes属性表n个字节attributes_count

Class 文件格式只有两种数据类型:无符号数和表

Class 文件获取方式:

img

案例

接下来以下面代码进行讲解类文件结构

package JJTest;

public class Demo {
    private int num = 1;
    public int add(){
        num = num + 2;
        return num;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

上面代码的字节码对应的16进制

img

魔数

每个 Class 文件开头的 4 个字节的无符号整数称为魔数(Magic Number),是 Class 文件的标识符,代表这是一个能被虚拟机接受的有效合法的 Class 文件,

版本

4 个 字节,5 6两个字节代表的是编译的副版本号 minor_version,7 8 两个字节是编译的主版本号 major_version

class="table-box">
主版本(十进制)副版本(十进制)编译器版本
4531.1
4601.2
4701.3
4801.4
4901.5
5001.6
5101.7
5201.8
5301.9
5401.10
5501.11

常量池计数器

常量池

constant_pool 是一种表结构,以 1 ~ constant_pool_count - 1 为索引,表明有多少个常量池表项。表项中存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池

常量池中常量类型

常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(tag),代表当前这个常量属于哪种常量类型。

img

18 种常量没有出现 byte、short、char,boolean 的原因:编译之后都可以理解为 Integer

访问标识

访问标识(access_flag),又叫访问标志、访问标记,该标识用两个字节表示,用于识别一些类或者接口层次的访问信息,包括这个 Class 是类还是接口,是否定义为 public类型,是否定义为 abstract类型等

class="table-box">
标志名称标志值含义
ACC_PUBLIC0x0001标志为 public 类型
ACC_FINAL0x0010标志被声明为 final,只有类可以设置
ACC_SUPER0x0020标志允许使用 invokespecial 字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真,使用增强的方法调用父类方法
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC0x1000标志此类并非由用户代码产生(由编译器产生的类,没有源码对应)
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUM0x4000标志这是一个枚举

索引集合

本类索引、父类索引、接口索引集合

class="table-box">
长度含义
u2this_class
u2super_class
u2interfaces_count
u2interfaces[interfaces_count]

字段表

字段 fields 用于描述接口或类中声明的变量,包括类变量以及实例变量,但不包括方法内部、代码块内部声明的局部变量(local variables)以及从父类或父接口继承。字段叫什么名字、被定义为什么数据类型,都是无法固定的,只能引用常量池中的常量来描述

fields_count(字段计数器),表示当前 class 文件 fields 表的成员个数,用两个字节来表示

fields[](字段表):

class="table-box">
标志名称标志值含义数量
u2access_flags访问标志1
u2name_index字段名索引1
u2descriptor_index描述符索引1
u2attributes_count属性计数器1
attribute_infoattributes属性集合attributes_count

字段访问标志:

class="table-box">
标志名称标志值含义
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSTENT0x0080字段是否为transient
ACC_SYNCHETIC0x1000字段是否为由编译器自动产生
ACC_ENUM0x4000字段是否为enum

字段名索引:根据该值查询常量池中的指定索引项即可

描述符索引:用来描述字段的数据类型、方法的参数列表和返回值

class="table-box">
字符类型含义
Bbyte有符号字节型树
CcharUnicode字符,UTF-16编码
Ddouble双精度浮点数
Ffloat单精度浮点数
Iint整型数
Jlong长整数
Sshort有符号短整数
Zboolean布尔值true/false
Vvoid代表void类型
L Classnamereference一个名为Classname的实例
[reference一个一维数组

属性表集合:属性个数存放在 attribute_count 中,属性具体内容存放在 attribute 数组中,一个字段还可能拥有一些属性,用于存储更多的额外信息,比如常量的初始化值、一些注释信息等,对于常量属性而言,attribute_length 值恒为2

ConstantValue_attribute{
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

方法表

方法表是 methods 指向常量池索引集合,其中每一个 method_info 项都对应着一个类或者接口中的方法信息,完整描述了每个方法的签名

要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存

methods_count(方法计数器):表示 class 文件 methods 表的成员个数,使用两个字节来表示

methods[](方法表):每个表项都是一个 method_info 结构,表示当前类或接口中某个方法的完整描述

方法表结构如下:

class="table-box">
类型名称含义数量
u2access_flags访问标志1
u2name_index字段名索引1
u2descriptor_index描述符索引1
u2attrubutes_count属性计数器1
attribute_infoattributes属性集合attributes_count

方法表访问标志:

class="table-box">
标志名称标志值含义
ACC_PUBLIC0x0001public,方法可以从包外访问
ACC_PRIVATE0x0002private,方法只能本类访问
ACC_PROTECTED0x0004protected,方法在自身和子类可以访问
ACC_STATIC0x0008static,静态方法

属性表

属性表集合,指的是 Class 文件所携带的辅助信息,比如该 Class 文件的源文件的名称,以及任何带有 RetentionPolicy.CLASS 或者 RetentionPolicy.RUNTIME 的注解,这类信息通常被用于 Java 虚拟机的验证和运行,以及 Java 程序的调试。字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息

attributes_ count(属性计数器):表示当前文件属性表的成员个数

attributes[](属性表):属性表的每个项的值必须是 attribute_info 结构

属性类型:

class="table-box">
属性名称使用位置含义
Code方法表Java 代码编译成的字节码指令
ConstantValue字段表final 关键字定义的常量池
Deprecated类、方法、字段表被声明为 deprecated 的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass类文件内部类列表
LineNumberTableCode 属性Java 源码的行号与字节码指令的对应关系
LocalVariableTableCode 属性方法的局部变量描述
StackMapTableCode 属性JDK1.6 中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature类,方法表,字段表用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件用于存储额外的调试信息
Syothetic类,方法表,字段表标志方法或字段为编泽器自动生成的
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持
RuntimelnvisibleAnnotations类,方法表,字段表用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation方法表作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法
RuntirmelnvisibleParameterAnniotation方法表作用与 RuntimelnvisibleAnnotations 属性类似,作用对象哪个为方法参数
AnnotationDefauit方法表用于记录注解类元素的默认值
BootstrapMethods类文件用于保存 invokeddynanic 指令引用的引导方式限定符

部分属性详解

① ConstantValue属性

ConstantValue属性表示一个常量字段的值。位于field_info结构的属性表中。

② Deprecated 属性

Deprecated 属性是在JDK1.1为了支持注释中的关键词@deprecated而引入的。

③ Code属性

Code属性就是存放方法体里面的代码。但是,并非所有方法表都有Code属性。像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了。Code属性表的结构,如下图:

class="table-box">
类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2max_stack1操作数栈深度的最大值
u2max_locals1局部变量表所需的存续空间
u4code_length1字节码指令的长度
u1codecode_lenth存储字节码指令
u2exception_table_length1异常表长度
exception_infoexception_tableexception_length异常表
u2attributes_count1属性集合计数器
attribute_infoattributesattributes_count属性集合

可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。

④ InnerClasses 属性

为了方便说明特别定义一个表示类或接口的Class格式为C。如果C的常量池中包含某个CONSTANT_Class_info成员,且这个成员所表示的类或接口不属于任何一个包,那么C的ClassFile结构的属性表中就必须含有对应的InnerClasses属性。InnerClasses属性是在JDK1.1中为了支持内部类和内部接口而引入的,位于ClassFile结构的属性表。

⑤ LineNumberTable属性

LineNumberTable属性是可选变长属性,位于Code结构的属性表。

LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。

在Code属性的属性表中,LineNumberTable属性可以按照任意顺序出现,此外,多个LineNumberTable属性可以共同表示一个行号在源文件中表示的内容,即LineNumberTable属性不需要与源文件的行一一对应。

⑥ LocalVariableTable属性

LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在Code属性的属性表中,LocalVariableTable属性可以按照任意顺序出现。Code属性中的每个局部变量最多只能有一个LocalVariableTable属性。

// LocalVariableTable属性表结构:
LocalVariableTable_attribute{
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {
        u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
⑦ Signature属性

Signature属性是可选的定长属性,位于ClassFile,field_info或method_info结构的属性表中。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。

⑧ SourceFile属性

class="table-box">
类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2sourcefile index1源码文件素引

可以看到,其长度总是固定的8个字节。

⑨ 其他属性

Java虚拟机中预定义的属性有20多个,这里就不一一介绍了,通过上面几个属性的介绍,只要领会其精髓,其他属性的解读也是易如反掌。

❷类加载时机

主动引用:对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化

  1. 当遇到

    new
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1

    getstatic
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1

    putstatic
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1

    invokestatic
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1

    这 4 条直接码指令时

  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。

  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  5. MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。

  6. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

被动引用:所有引用类的方式都不会触发初始化,称为被动引用

❸类加载过程

0.生命周期

在Java中数据类型分为基本数据类型引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:

img

1.加载

加载过程完成以下三件事:

  1. 通过全限定类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构(Java类模型)
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

二进制字节流可以从以下方式中获取:

方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构:

加载过程:

class="table-box">
类实例&类模型位置加载过程
imgimg

数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程:

2.链接

2.1验证

确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全

主要包括四种验证

2.2准备

为类变量/静态变量分配内存并设置默认初始值的阶段,使用的是方法区的内存。

说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次

类变量初始化:

实例:

class="table-box">
类型默认初始值
byte(byte)0
short(short)0
int0
long0L
float0.0f
double0.0
char\u0000
booleanfalse
referencenull
2.3解析

将常量池中类、接口、字段、方法的符号引用替换为直接引用(内存地址)的过程。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等

3.初始化

初始化阶段是执行初始化方法 ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 () ,另一个是实例的初始化方法 ()

类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机调用一次,后续实例化不再加载,引用第一次加载的类,而实例构造器则会被虚拟机调用多次,只要程序员创建对象

3.1 clinit

():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的

作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块

线程安全问题:

特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

public class Test {
    static {
        //i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  	// 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

接口中不可以使用静态语句块,但有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法

3.3 init

() 指实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行

实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行

new 关键字会创建对象并复制 dup 一个对象引用,一个调用 `` 方法,另一个用来赋值给接收者

4.卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足3个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

❹类实例化过程

父类的类构造器 ➔ 子类的类构造器➔ 父类的实例构造器 ➔ 父类的构造函数 ➔ 子类的的实例构造器 ➔ 子类的构造函数

//父类
public class Father {
    static {
        System.out.println("父静态代码块");
    }

    {
        System.out.println("父非静态代码块");
    }

    public Father(){
        System.out.println("父构造器");
    }
}
//子类
public class Son extends Father {
    static {
        System.out.println("子静态代码块");
    }

    {
        System.out.println("子非静态代码块");
    }

    public Son(){
        System.out.println("子构造器");
    }
  
    //创建对象
    public static void main(String[] args) {
        new Son();
    }
}
/**
  运行结果:
  父静态代码块
  子静态代码块
  父非静态代码块
  父构造器
  子非静态代码块
  子构造器
**/
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

❺类加载器

0.基础知识

类加载分类
//隐式加载
User user = new User();
//显式加载,并初始化
Class clazz = Class.forName("com.test.java.User");
//显式加载,但不初始化
ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

类加载器基本特征:

ClassLoader

ClassLoader 类,是一个抽象类,除启动类加载器外其它类加载器都继承自 ClassLoader

获取 ClassLoader 的途径:

ClassLoader 类常用方法:

类加载模型

在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制

1.加载器

类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象

从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器:

从 Java 开发人员的角度看:

img

public class ClassLoaderTest {
    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层  扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6

        //获取其上层 获取不到启动类加载器
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println(bootStrapClassLoader);//null

        //对于用户自定义类来说:使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String 类使用引导类加载器进行加载的 --> java核心类库都是使用启动类加载器加载的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

2.双亲委派

img

双亲委派机制的优点:

双亲委派机制的缺点:

检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类(可见性)

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。

参考:https://m.imooc.com/wiki/jvm-loadparent

3.源码分析

双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)方法中体现。逻辑如下:

  1. 先在当前加载器的缓存中查找有无目标类 findLoadedClass(name),如果有,直接返回。
  2. 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)接口进行加载。
  3. 如果当前加载器的父加载器为空,则调用findBootstrapClassorNull(name)接口,让启动类加载器进行加载。
  4. 如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。

双亲委派的模型就隐藏在这第2和第3步中。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
       // 1.调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类
        Class c = findLoadedClass(name);
        
        // 当前类加载器如果没有加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 2.判断当前类加载器是否有父类加载器
                if (parent != null) {
                    // 如果当前类加载器有父类加载器,则调用父类加载器的 loadClass(name,false)
         			     // 父类加载器的 loadClass 方法,又会检查自己是否已经加载过
                    c = parent.loadClass(name, false);
                } else {
                    // 3.当前类加载器没有父类加载器,说明当前类加载器是 BootStrapClassLoader
          			   // 则调用 BootStrap ClassLoader 的方法加载类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) { }

            if (c == null) {
                long t1 = System.nanoTime();
                // 4.如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载
                // 可以自定义 findClass() 方法
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析
            resolveClass(c);
        }
        return c;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

4.破坏委派

双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式

破坏双亲委派模型的方式:

img

5.自定义加载器

对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可

作用:隔离加载类、修改类加载的方式、拓展加载源、防止源码泄漏

//自定义类加载器,读取指定的类路径classPath下的class文件
public class MyClassLoader extends ClassLoader{
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
     public MyClassLoader(ClassLoader parent, String classPath) {
        super(parent);
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
       BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            // 获取字节码文件的完整路径
            String fileName = classPath + className + ".class";
            // 获取一个输入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            // 获取一个输出流
            baos = new ByteArrayOutputStream();
            // 具体读入数据并写出的过程
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            // 获取内存中的完整的字节数组的数据
            byte[] byteCodes = baos.toByteArray();
            // 调用 defineClass(),将字节数组的数据转换为 Class 的实例。
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            return clazz;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
public static void main(String[] args) {
    MyClassLoader loader = new MyClassLoader("/Workspace/Project/JVM_study/src/java1/");

    try {
        Class clazz = loader.loadClass("Demo1");
        System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//MyClassLoader

        System.out.println("加载当前类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

6.JDK9新特性

为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动:

⑤程序编译

Java 代码执行流程:Java 程序(.java) --(编译)--> 字节码文件(.class)--(解释执行/JIT)--> 操作系统(Win,Linux)

❶字节码指令集

Java 字节码由操作码和操作数组成。

由于 Java 虚拟机是基于栈而不是寄存器的结构,所以大多数指令都只有一个操作码。比如 aload_0(将局部变量表中下标为 0 的数据压入操作数栈中)就只有操作码没有操作数,而 invokespecial #1(调用成员方法或者构造方法,并传递常量池中下标为 1 的常量)就是由操作码和操作数组成的。

0.字节码与数据类型

在 JVM 的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 指令加载的则是 float 类型的数据

大部分的指令都没有支持 byte、char、short、boolean 类型,编译器会在编译期或运行期将 byte 和 short 类型的数据带符号扩展(Sign-Extend-)为相应的 int 类型数据,将 boolean 和 char 类型数据零位扩展(Zero-Extend)为相应的 int 类型数据

在做值相关操作时:

1.加载与存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递

局部变量压栈指令:将给定的局部变量表中的数据压入操作数栈

常量入栈指令:将常数压入操作数栈,根据数据类型和入栈内容的不同,又分为 const_pushldc 指令

出栈装入局部变量表指令:将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值

扩充局部变量表的访问索引的指令wide

2.算术指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算结果重新压入操作数栈

没有直接支持 byte、 short、 char 和 boolean 类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理,数组类型也是转换成 int 数组

运算模式:

NaN 值:当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将使用 NaN 值来表示

double j = i / 0.0;
System.out.println(j);//无穷大,NaN: not a number
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

分析 i++:从字节码角度分析:a++ 和 ++a 的区别是先执行 iload 还是先执行 iinc

 4 iload_1		//存入操作数栈
 5 iinc 1 by 1	//自增i++
 8 istore_3		//把操作数栈没有自增的数据的存入局部变量表
   
 9 iinc 2 by 1	//++i
12 iload_2		//加载到操作数栈
13 istore 4		//存入局部变量表,这个存入没有 _ 符号,_只能到3
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
public class Demo {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);	//11
        System.out.println(b);	//34
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

判断结果:

public class Demo {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x); // 结果是 0
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

3.类型转换指令

类型转换指令可以将两种不同的数值类型进行相互转换,除了 boolean 之外的七种类型

宽化类型转换:

窄化类型转换:


4.对象的创建与访问指令

5.方法调用与返回指令

方法调用指令

普通调用指令:

动态调用指令:

指令对比:

指令说明:

方法返回指令

方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。

class="table-box">
方法返回指令voidintlongfloatdoublereference
xreturnreturnireturnlreturnfreutrndreturnareturn

ireturn(当返回值是boolean、byte、char、short和int 类型时使用)

通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。

如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。

最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。

6.操作数栈管理指令

JVM 提供的操作数栈管理指令,可以用于直接操作操作数栈的指令

7.比较控制指令

比较指令:比较栈顶两个元素的大小,并将比较结果入栈

条件跳转指令:

class="table-box">
指令说明
ifeqequals,当栈顶int类型数值等于0时跳转
ifnenot equals,当栈顶in类型数值不等于0时跳转
ifltlower than,当栈顶in类型数值小于0时跳转
iflelower or equals,当栈顶in类型数值小于等于0时跳转
ifgtgreater than,当栈顶int类型数组大于0时跳转
ifgegreater or equals,当栈顶in类型数值大于等于0时跳转
ifnull为 null 时跳转
ifnonnull不为 null 时跳转

比较条件跳转指令:

class="table-box">
指令说明
if_icmpeq比较栈顶两 int 类型数值大小(下同),当前者等于后者时跳转
if_icmpne当前者不等于后者时跳转
if_icmplt当前者小于后者时跳转
if_icmple当前者小于等于后者时跳转
if_icmpgt当前者大于后者时跳转
if_icmpge当前者大于等于后者时跳转
if_acmpeq当结果相等时跳转
if_acmpne当结果不相等时跳转

多条件分支跳转指令:

无条件跳转指令:

8.异常处理指令

抛出异常指令:athrow 指令,在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。

处理异常

JVM 处理异常(catch 语句)不是由字节码指令来实现的,而是采用异常表来完成

Exception table:  
    from   to 	target 	type
        2	   5 		 11 	  Class java/lang/Exception
        2 	 5 		 21 	  any // 剩余的异常类型,比如 Error
     11   15 		 21 	  any // 剩余的异常类型,比如 Error
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

9.同步控制指令

Java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的

方法级的同步:是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法

方法内指定指令序列的同步:有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义

img

🌟图解字节码运行

1)原始java 代码

/*
    演示 字节码指令 和 操作数栈、常量池的关系
 */
public class Demo {
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1; //32767 + 1
        int c = a + b;
        System.out.println(c);
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

2)编译后的核心字节码文件

  MD5 checksum cc8fc12b6e178b8f28e787497e993363
  Compiled from "Demo.java"
public class com.jvm.test.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // com/jvm/test/Demo
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/jvm/test/Demo;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo.java
  #25 = NameAndType        #8:#9          // "":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               com/jvm/test/Demo
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public com.jvm.test.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jvm/test/Demo;
 
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4        // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5        // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 17: 0
        line 18: 3
        line 19: 6
        line 20: 10
        line 21: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo.java"
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

3)常量池载入运行时常量池

img

4)方法字节码载入方法区

img

5)main线程开始运行,分配栈帧内存(stack=2, locals=4)

img

6)执行引擎开始执行字节码

bipush 10

img

istore_1

img

img

idc #3

img

istore_2

img

img

iload_1

再接下来,需要执行int c = a + b;执行引擎不能直接在局部变量表进行a+b操作,需要先将a、b进行读取,然后放入操作数栈中才能进行计算分析

img

iload 2

img

iadd

img

img

istore_3

img

img

getstatic #4

img

img

iload_3

img

img

invokevirtual #5

img

img

return

参考:Java字节码的一段旅行经历——提升硬实力1

🌟a++ 字节码分析

在日常的项目开发中,经常遇到a++、++a、a–之类,下面我们开始从字节码的视角来分析a++。

java代码如下:

/*
 从字节码角度分析  a++ 相关题目
 */
public class Demo {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

使用javap -v xxx.class 来查看类文件全部指令信息,如下:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3          // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3          // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 17: 0
        line 18: 3
        line 19: 18
        line 20: 25
        line 21: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
}
SourceFile: "Demo.java"
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

分析:

下面我们通过字节码来剖析如下两行代码在内存当中整个执行过程

int a = 10;
int b = a++ + ++a + a--;
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

下图是先将10通过bipush 放入操作数栈中

img

接着将10从操作数栈上弹出存入局部变量表1号槽位,相当于代码 int a = 10 执行完成

img

接着执行:int b = a++ + ++a + a–; 因为有从左往右的执行顺序,所以先执行a++,先将a的值加载到操作数栈中;通过iload_1加载1号槽位的数据到操作数栈中

img

接着执行a++自增1操作,这个操作是在局部变量表中完成的。相当于完成了a++执行

img

再接着执行++a自增1操作,这个操作也是在局部变量表中完成的

img

接着从局部变量表1号槽位加载数据到操作数栈中,即12入栈,完成发a++ 、++a 各自的执行了

img

然后,iadd是将操作数栈中弹出(出栈)两个数12、10进行求和操作,得到22,最后将累加的结果22存入栈中。即完成了a++ + ++a的执行

img

接着,需要执行a–,先将局部变量表槽位1的数据12加载到操作数栈中

img

然后,将局部变量表槽位1的数据自减1

img

接着,执行iadd操作,将操作数栈12、22弹出栈后,进行求和操作得到34,再将34结果压入栈

img

最后,执行istore_2操作,将操作数栈弹出数据34,并压入局部变量表2号槽位中

img

img

参考:Java字节码角度分析a++ ——提升硬实力2

❷编译器

C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度。

C1 编译器的优化方法:

C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高,当激进优化的假设不成立时,再退回使用 C1 编译,这也是使用分层编译的原因

C2 的优化主要是在全局层面,逃逸分析是优化的基础,如果不存在逃逸行为,则可进行如下优化:

❸执行引擎(解释+JIT)

Java 是半编译半解释型语言,将解释执行与编译执行二者结合起来进行:

HostSpot 的默认执行方式:

img

HotSpot 可以通过 VM 参数设置程序执行方式:

❹分层编译

在分层编译的工作模式出现前,采用客户端编译器还是服务端编译器完全取决于虚拟机是运行在客户端模式还是服务端模式下,可以在启动时通过 -client-server 参数进行指定,也可以让虚拟机根据自身版本和宿主机性能来自主选择。

要编译出优化程度越高的代码通常都需要越长的编译时间,为了在程序启动速度与运行效率之间达到最佳平衡,HotSpot 虚拟机在编译子系统中加入了分层编译(Tiered Compilation),JVM 将执行状态分成了 5 个层次:

C1 编译器会对字节码进行简单可靠的优化,耗时短,以达到更快的编译速度

C2 编译器进行耗时较长的优化以及激进优化,优化的代码执行效率更高

实施分层编译后,解释器、C1编译器和C2编译器就会同时工作,可以用C1编译器获取更高的编译速度、用C2编译器来获取更好的编译质量。

❺热点探测

即时编译器编译的目标是 “热点代码”,它主要分为以下两类:

判断某段代码是否是热点代码的行为称为 “热点探测” (Hot Spot Code Detection),主流的热点探测方法有以下两种:

HotSpot VM 采用的热点探测方式是基于计数器的热点探测,为每一个方法都建立 2 个不同类型的计数器:方法调用计数器(Invocation Counter)和回边计数器(BackEdge Counter)

⑥代码优化

❶语法糖

语法糖:指 Java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担

构造器

public class Candy {
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
public class Candy {
    // 这个无参构造是编译器帮助我们加上的
    public Candy() {
        super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."":()V
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

拆装箱

这段代码在 JDK 5 之前是无法编译通过的

Integer x = 1;
int y = x;
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

必须写成如下代码:

Integer x = Integer.valueOf(1); //装箱
int y = x.intValue(); //拆箱
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

JDK5 以后编译阶段自动转换成上述片段

泛型擦除

泛型也是在 JDK 5 开始加入的特性,但 Java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

编译器真正生成的字节码中,还要额外做一个类型转换的操作:

// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

可变参数

public class Candy {
    public static void foo(String... args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo("hello", "world");
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

可变参数 String... args 其实是 String[] args , Java 编译器会在编译期间将上述代码变换为:

public static void main(String[] args) {
    foo(new String[]{"hello", "world"});
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

注:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

foreach

数组的循环:

int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖
for (int e : array) {
    System.out.println(e);
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

编译后为循环取数:

for(int i = 0; i < array.length; ++i) {
    int e = array[i];
    System.out.println(e);
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

集合的循环:

List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
    System.out.println(i);
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

编译后转换为对迭代器的调用:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while(iter.hasNext()) {
    Integer e = (Integer)iter.next();
    System.out.println(e);
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

注:foreach 循环写法,能够配合数组以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器

switch

switch 可以作用于字符串和枚举类:

字符串
switch (str) {
    case "hello": {
        System.out.println("h");
        break;
    }
    case "world": {
        System.out.println("w");
        break;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

注意:switch 配合 String 和枚举使用时,变量不能为 null

会被编译器转换为:

byte x = -1;
switch(str.hashCode()) {
    case 99162322: // hello 的 hashCode
        if (str.equals("hello")) {
            x = 0;
        }
        break;
    case 113318802: // world 的 hashCode
        if (str.equals("world")) {
            x = 1;
        }
}
switch(x) {
    case 0:
        System.out.println("h");
        break;
    case 1:
        System.out.println("w");
        break;
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

总结:

枚举

switch 枚举的例子,原始代码:

enum Sex {
    MALE, FEMALE
}
public class Candy {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男"); 
                break;
            case FEMALE:
                System.out.println("女"); 
                break;
        }
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

编译转换后的代码:

/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
    // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字
    static int[] map = new int[2];
    static {
        map[Sex.MALE.ordinal()] = 1;
        map[Sex.FEMALE.ordinal()] = 2;
    }
}
public static void foo(Sex sex) {
    int x = $MAP.map[sex.ordinal()];
    switch (x) {
        case 1:
            System.out.println("男");
            break;
        case 2:
            System.out.println("女");
            break;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

枚举类

JDK 7 新增了枚举类:

enum Sex {
    MALE, FEMALE
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

编译转换后:

public final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;
    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }
    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }
    public static Sex[] values() {
        return $VALUES.clone();
    }
    public static Sex valueOf(String name) {
        return Enum.valueOf(Sex.class, name);
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

try-w-r

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources,格式:

try(资源变量 = 创建资源对象){
} catch( ) {
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码:

try(InputStream is = new FileInputStream("a.txt")) {
    System.out.println(is);
} catch (IOException e) {
    e.printStackTrace();
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

转换成:

try {
    InputStream is = new FileInputStream("a.txt");
    Throwable t = null;
    try {
        System.out.println(is);
    } catch (Throwable e1) {
        // t 是我们代码出现的异常
        t = e1;
        throw e1;
    } finally {
        // 判断了资源不为空
        if (is != null) {
            // 如果我们代码有异常
            if (t != null) {
                try {
                    is.close();
                } catch (Throwable e2) {
                    // 如果 close 出现异常,作为被压制异常添加
                    t.addSuppressed(e2);
                }
            } else {
                // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
                is.close();
            }
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

addSuppressed(Throwable e):添加被压制异常,是为了防止异常信息的丢失(fianlly 中如果抛出了异常

方法重写

方法重写时对返回值分两种情况:

class A {
    public Number m() {
            return 1;
    }
}
class B extends A {
    @Override
    // 子类m方法的返回值是Integer是父类m方法返回值Number的子类
    public Integer m() {
        return 2;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

对于子类,Java 编译器会做如下处理:

class B extends A {
    public Integer m() {
        return 2;
    }
    // 此方法才是真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() {
        // 调用 public Integer m()
        return m();
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

其中桥接方法比较特殊,仅对 Java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突

匿名内部类

无参优化

源代码:

public class Candy {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

转化后代码:

// 额外生成的类
final class Candy$1 implements Runnable {
    Candy$1() {
    }
    public void run() {
        System.out.println("ok");
    }
}
public class Candy {
    public static void main(String[] args) {
        Runnable runnable = new Candy$1();
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
带参优化

引用局部变量的匿名内部类,源代码:

public class Candy {
    public static void test(final int x) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
            }
        };
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

转换后代码:

final class Candy$1 implements Runnable {
    int val$x;
    Candy$1(int x) {
        this.val$x = x;
    }
    public void run() {
        System.out.println("ok:" + this.val$x);
    }
}
public class Candy {
    public static void test(final int x) {
        Runnable runnable = new Candy$1(x);
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

局部变量在底层创建为内部类的成员变量,必须是 final 的原因:

反射优化

public class Reflect1 {
    public static void foo() {
        System.out.println("foo...");
    }
    public static void main(String[] args) throws Exception {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

foo.invoke 0 ~ 15 次调用的是 MethodAccessor 的实现类 NativeMethodAccessorImpl.invoke0(),本地方法执行速度慢;当调用到第 16 次时,会采用运行时生成的类 sun.reflect.GeneratedMethodAccessor1 代替

public Object invoke(Object obj, Object[] args)throws Exception {
    // inflationThreshold 膨胀阈值,默认 15
    if (++numInvocations > ReflectionFactory.inflationThreshold()
        && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
        MethodAccessorImpl acc = (MethodAccessorImpl)
            new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                           method.getName(),
                           method.getParameterTypes(),
                           method.getReturnType(),
                           method.getExceptionTypes(),
                           method.getModifiers());
        parent.setDelegate(acc);
    }
    // 【调用本地方法实现】
    return invoke0(method, obj, args);
}
private static native Object invoke0(Method m, Object obj, Object[] args);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    // 如果有参数,那么抛非法参数异常
    block4 : {
        if (arrobject == null || arrobject.length == 0) break block4;
            throw new IllegalArgumentException();
    }
    try {
        // 【可以看到,已经是直接调用方法】
        Reflect1.foo();
        // 因为没有返回值
        return null;
    }
   //....
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

通过查看 ReflectionFactory 源码可知:

❷运行期优化(JIT优化)

即时编译器除了将字节码编译为本地机器码外,还会对代码进行一定程度的优化,它包含多达几十种优化技术,这里选取其中代表性的进行介绍:

逃逸分析

逃逸分析并不是直接的优化手段,而是一个代码分析方式,通过动态分析对象的作用域,为优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸

public static StringBuilder concat(String... strings) {
    StringBuilder sb = new StringBuilder();
    for (String string : strings) {
        sb.append(string);
    }
    return sb; // 发生了方法逃逸
}

public static String concat(String... strings) {
    StringBuilder sb = new StringBuilder();
    for (String string : strings) {
        sb.append(string);
    }
    return sb.toString(); // 没有发生方法逃逸
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配

方法内联

方法内联:将调用的函数代码编译到调用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程

方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。

private static int square(final int i) {
    return i * i;
}
System.out.println(square(9));
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置:

System.out.println(9 * 9);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

还能够进行常量折叠(constant folding)的优化:

System.out.println(81);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

公共子表达式消除

如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。

数组边界检查消除

对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断。

⑦性能监控&调优

❶监控&诊断工具

命令行工具

参考:Java问题诊断和排查工具

img

GUI工具

参考:JVM监控及诊断工具-GUI篇

JConsole

从Java5开始,在JDK中自带的Java监控和管理控制台。用于对JVM中内存、线程和类等的监控。

Visual VM

多功能的监测工具,可以连续监测,集成了多个JDK命令行工具,用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。

JProfiler

主要功能:

1-方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法

2-内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用

3-线程和锁:JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题

4-高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析

Arthas

Arthas是Alibaba开源的Java诊断工具

HSDB

JDK自带的工具,用于查看JVM运行时的状态

java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_351.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

❷JVM运行时参数

JVM参数⼤致可以分为三类:

  1. 标注指令: -开头,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。可以⽤ java -help 打印出来。

  2. ⾮标准指令: -X开头,默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容。可以⽤ java -X 打印出来。

  3. 非稳定参数: -XX 开头,此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;

    java -XX:+PrintCommandLineFlags  // 查看当前JVM的不稳定指令。
    
    java -XX:+PrintFlagsInitial     // 查看所有不稳定指令的默认值。
    
    java -XX:+PrintFlagsFinal       // 查看所有不稳定指令最终⽣效的实际值。
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5

含义参数

堆初始大小-Xms

堆最大大小-Xmx 或 -XX:MaxHeapSize=size

新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )

幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

幸存区比例-XX:SurvivorRatio=ratio

晋升阈值-XX:MaxTenuringThreshold=threshold

晋升详情-XX:+PrintTenuringDistribution

GC详情-XX:+PrintGCDetails -verbose:gc

FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

❸GC日志

📚参考资料


如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配

- **栈上分配 (Stack Allocations)**:如果一个对象不会逃逸到线程外,那么将会在栈上分配内存来创建这个对象,而不是 Java 堆上,此时对象所占用的内存空间就会随着栈帧的出栈而销毁,从而可以减轻垃圾回收的压力。
- **标量替换 (Scalar Replacement)**:如果一个数据已经无法再分解成为更小的数据类型,那么这些数据就称为标量(如 int、long 等数值类型及 reference 类型等);反之,如果一个数据可以继续分解,那它就被称为聚合量(如对象)。如果一个对象不会逃逸外方法外,那么就可以将其改为直接创建若干个被这个方法使用的成员变量来替代,从而减少内存占用。
  - `-XX:+EliminateAllocations`:开启标量替换
  - `-XX:+PrintEliminateAllocations`:查看标量替换情况
- **同步消除 (Synchronization Elimination)\**:线程同步本身比较耗时,如果确定一个对象不会逃逸出线程,不被其它线程访问到,那对象的读写就不会存在竞争,则可以消除对该对象的\**同步锁**,通过 `-XX:+EliminateLocks` 可以开启同步消除 ( - 号关闭)

### 方法内联

方法内联:**将调用的函数代码编译到调用点处**,这样可以减少栈帧的生成,减少参数传递以及跳转过程

方法内联能够消除方法调用的固定开销,任何方法除非被内联,否则调用都会有固定开销,来源于保存程序在该方法中的执行位置,以及新建、压入和弹出新方法所使用的栈帧。



```java
private static int square(final int i) {
    return i * i;
}
System.out.println(square(9));
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}"> class="hide-preCode-box">

square 是热点方法,会进行内联,把方法内代码拷贝粘贴到调用者的位置:

System.out.println(9 * 9);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

还能够进行常量折叠(constant folding)的优化:

System.out.println(81);
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

公共子表达式消除

如果一个表达式 E 之前已经被计算过了,并且从先前的计算到现在 E 中所有变量的值都没有发生过变化,那么 E 这次的出现就称为公共子表达式。对于这种表达式,无需再重新进行计算,只需要直接使用前面的计算结果即可。

数组边界检查消除

对于虚拟机执行子系统来说,每次数组元素的读写都带有一次隐含的上下文检查以避免访问越界。如果数组的访问发生在循环之中,并且使用循环变量来访问数据,即循环变量的取值永远在 [0,list.length) 之间,那么此时就可以消除整个循环的数据边界检查,从而避免多次无用的判断。

⑦性能监控&调优

❶监控&诊断工具

命令行工具

参考:Java问题诊断和排查工具

[外链图片转存中…(img-J35iyIwS-1736236791653)]

GUI工具

参考:JVM监控及诊断工具-GUI篇

JConsole

从Java5开始,在JDK中自带的Java监控和管理控制台。用于对JVM中内存、线程和类等的监控。

Visual VM

多功能的监测工具,可以连续监测,集成了多个JDK命令行工具,用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。

JProfiler

主要功能:

1-方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法

2-内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用

3-线程和锁:JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题

4-高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析

Arthas

Arthas是Alibaba开源的Java诊断工具

HSDB

JDK自带的工具,用于查看JVM运行时的状态

java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_351.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

❷JVM运行时参数

JVM参数⼤致可以分为三类:

  1. 标注指令: -开头,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。可以⽤ java -help 打印出来。

  2. ⾮标准指令: -X开头,默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容。可以⽤ java -X 打印出来。

  3. 非稳定参数: -XX 开头,此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;

    java -XX:+PrintCommandLineFlags  // 查看当前JVM的不稳定指令。
    
    java -XX:+PrintFlagsInitial     // 查看所有不稳定指令的默认值。
    
    java -XX:+PrintFlagsFinal       // 查看所有不稳定指令最终⽣效的实际值。
     class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
    • 1
    • 2
    • 3
    • 4
    • 5

含义参数

堆初始大小-Xms

堆最大大小-Xmx 或 -XX:MaxHeapSize=size

新生代大小-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )

幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy

幸存区比例-XX:SurvivorRatio=ratio

晋升阈值-XX:MaxTenuringThreshold=threshold

晋升详情-XX:+PrintTenuringDistribution

GC详情-XX:+PrintGCDetails -verbose:gc

FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

❸GC日志

📚参考资料

data-report-view="{"mod":"1585297308_001","spm":"1001.2101.3001.6548","dest":"https://blog.csdn.net/qq_42865148/article/details/144987594","extend1":"pc","ab":"new"}">>
注:本文转载自blog.csdn.net的时晴⁧⁧的文章"https://blog.csdn.net/qq_42865148/article/details/144987594"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接

评论记录:

未查询到任何数据!