当前位置: 首页 > news >正文

jvm相关知识详解

jvm相关知识详解

  • 1.hello world!?
  • 2.JVM数据区域划分
      • 2.1 程序计数器
      • 2.2 Java虚拟机栈
      • 2.3 本地方法栈
      • 2.4 java堆
      • 2.5 方法区
      • 2.6 直接内存
      • 2.7 对象内存布局
      • 2.8 对象的访问定位
  • 3.对象回收时机
      • 3.1 垃圾回收概述
      • 3.2 引用计数算法
      • 3.3 可达性分析算法
      • 3.4再谈引用
  • 4.Hotspot算法细节实现
    • 4.1 根节点枚举
    • 4.2安全点
    • 4.3 安全区域
    • 4.4 记忆集与卡表
    • 4.5 写屏障
    • 4.6 并发的可达性分析
  • 5.垃圾回收算法
    • 4.1 标记清除算法
    • 4.2 标记复制算法
    • 4.3 标记整理算法
  • 6.分代垃圾回收
    • 6.1 JVM的一些相关参数
    • 6.2 GC分析
    • 6.3 垃圾回收器的分类
      • 6.3 .1 串行GC
      • 6.3 .2 吞吐量优先GC
      • 6.3 .1 响应时间优先GC
    • 6.4HotSpot 虚拟机中的GC
      • 6.4.1 Serial
      • 6.4.2 Serial Old
      • 6.4.3 ParNew
      • 6.4.4 Parallel Old
      • 6.4.5 Parallel Scavenge
      • 6.4.6 CMS
      • 6.4.7 Garbage First
      • 6.4.8 Full GC
      • 6.4.9 jdk8u20 字符串去重
      • 6.4.10 JDK 8u40 并发标记类卸载
      • 6.4.11 JDK 9 并发标记起始时间的调整
  • 7.GC调优(Hotspot)
    • 7.1确定目标
    • 5.2 最快的GC是不发生GC
    • 5.3 新生代调优
    • 5.4 老年代调优
    • 5.5 调优案例
  • 8.类加载和字节码技术
    • 8.1类文件结构
      • 8.1.1 魔数
      • 8.1.2 版本
      • 8.1.3 常量池表
      • 8.1.4 访问标识
      • 8.4.5 类索引,父类索引,接口索引集合
      • 8.4.6 字段表集合
      • 8.4.7 方法表
      • 8.7.8 属性表
    • 8.2字节码指令
      • 8.2.1 图解方法执行流程
      • 8.2.2 练习:分析i++
      • 8.2.3 条件判断指令
      • 8.2.4 循环控制命令
      • 8.2.5 构造方法
      • 8.2.6 方法调用
      • 8.2.7 多态原理
      • 8.2.8 异常处理
      • 8.2.9 synchronized
    • 8.3 编译期处理
      • 8.3.1 默认构造器
      • 8.3.2 自动拆装箱
      • 8.3.3 泛型集合取值
      • 8.3.4 可变参数
      • 8.3.5 foreach 循环
      • 8.3.6 switch 字符串
      • 8.3.7 switch 枚举
      • 8.3.8 枚举类
      • 8.2.9 try-with-resources
      • 8.2.10 0 方法重写时的桥接方法
      • 8.2.11 匿名内部类
    • 8.4 类加载阶段
      • 8.4.1 加载
      • 8.4.2 链接
      • 8.4.3 初始化
    • 8.4.5 类加载器
      • 8.4.5.1 双亲委派模式
      • 8.4.5.2 线程上下文加载器
      • 8.4.5.3 自定义类加载器
    • 8.6 运行期优化
      • 8.6.1 即时编译
      • 8.6.2 方法内联
      • 8.6.3 字段优化
      • 8.6.4 反射优化
  • 9. JVM 性能检测工具
  • 附录:
    • 1.jvm参数
    • 1.参考文档
    • 2.jvm助记符说明

1.hello world!?

我相信大多数人学一门语言都是先从 hello world 开始的,如果成功运行hello world 那么恭喜你,成功进入编程世界的大门。

public static void main(String[] args) {
        System.out.println("hello world!");
    }

当我们学了一门语言后,学习了API?而且会调用API,那么想要更进一步写出好的代码,那就得学习一下jvm了,就比如说你遇到的 StackOverflowError是如何引起的? 我们所定义的变量是存在什么位置的? 对象什么时候被垃圾回收器回收?这一系列问题学完jvm就有了一个新的理解,也能写出比较高效的代码,遇到问题也能快速定位。

我们的cpu只认识机器码:也就是由0和1组成的指令,那么我们编写的hello world 是如何交给cpu进行运算的呢?

image-20211212230420976

为什么说java是跨平台语言?(一次编译到处运行)

这个夸平台是中间语言(JVM)实现的夸平台
java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统,我们自己写的java源代码不用更改,只需要更换不同的jdk即可。

难道 C 和 C++ 不能夸平台吗 其实也可以,C和C++需要在编译器层面去兼容不同操作系统的不同层面,写过C和C++的就知道不同操作系统的有些代码是不一样。

Jdk和Jre和JVM的区别

看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM

Jvm在倒数第二层 由他可以在(最后一层的)各种平台上运行

Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库

Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具

image-20211212230657123

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

常见的jvm,注意不同版本的的jdk,jvm实现是不一样的。该文章基于HotSpot实现来描述的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aCHxiblr-1666016850022)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20211212230847531.png)]

2.JVM数据区域划分

首先我们来看下VM的一个结构图:接下来会根据这个结构图进行详细的一个说明

image-20211212234733934

2.1 程序计数器

**程序计数器(Program Counter Register):**了解程序计数器的概念之前,我们先来了解下栈数据结构,便于我们理解程序计数器。

image-20211213005551373

程序计数器是一块比较小的内存空间,是线程私有的,每个线程都有自己的一个程序计数器,可以看成是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过这个程序计数器来取下一条需要执行的字节码指令,程序计数器是程序控制的指示器,分支,循环,跳转,异常处理,线程恢复等基础的功能都需要这个程序计数器来完成。由于java多线程是通过线程的切换,分配处理器的执行时间来实现的,一个处理器(对于多核的处理器来说是一个内核)都只会执行一条线程中的指令,因此为了线程之间相互切换执行后能恢复到正确的执行位置,每一个线程都需要一个自己独立的程序计数器,各条线程之间程序计数器互不影响,数据存储独立,我称这类内存区域为:线程私有的内存,程序计数器在物理上是通过寄存器来实现的,寄存器是读取速度非常快的。

  • 程序计数器的作用:记住下一条指令的执行地址
  • 程序计数器的特点:线程私有,每个线程都有自己的程序计数器,不会出现内存溢出(OutOfMemoryError)的数据区域

首先我们来了解一下jvm字节码指令,这就是通过 javap -c Test.class这个指令来进行返编译的

 public com.compass.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_2
       1: invokestatic  #2                  // Method methodOne:(I)Z
       4: pop
       5: return

  public static boolean methodOne(int);
    Code:
       0: iload_0
       1: invokestatic  #3                  // Method methodTwo:(I)V
       4: iload_0
       5: iconst_3
       6: if_icmpne     13
       9: iconst_1
      10: goto          14
      13: iconst_0
      14: ireturn

  public static void methodTwo(int);
    Code:
       0: return
}

2.2 Java虚拟机栈

**Java虚拟机栈(Java Virtual Machine Stack) : **

与程序计数器一样,java虚拟机栈也是线程私有的,他的生命周期与线程相同,线程结束java虚拟机栈也就随之消失。虚拟机栈是描述java方法执行的线程内存模型,每个方法被执行的时候java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈(对数据进行计算的一个区域),动态链接,方法返回地址,每个方法被调用直至执行完毕的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程

局部变量表存放了编译器可知等待各种Java虚拟机基本数据类型(int,byte,char,long,double,boolean,short,float),对象引用(Refernce,他并不等同于对象本身,可能是一个指向对象起始地址的一个引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置和ReturnAddress[指向了一条字节码指令地址])

这些数据类型在局部变量表的储存空间以局部变量槽(Slot)来表示,其中64位长度的long和double占用两个变量槽,其余的数据类型只占用一个局部变量表所需空间在编译器完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是确定的,方法在运行期间不会改变局部变量表的大小

补充一点:基本数据类型存放的位置不是完全在java虚拟机栈中的,具体的话要看这个变量声明在说明位置

  • 第一种:类变量,由static关键字修饰的成员变量,随之类的加载而加载,存放在方法区中,可以通过类名.变量名直接调用
  • 第二种:类成员变量,随对象的创建而加载,对象存放在堆中(对象的引用存放在栈中)
  • 第三种:在方法内部声明,存放在栈中,随方法的调用而入栈,方法调用完出栈该变量也就声明周期结束

在<<java虚拟机规范>>中规定java虚拟机栈中出现的两类异常:

  • 一:如果线程请求的栈深度大于java虚拟机锁规定的大小,将抛出StackOverflow异常(最常见的就是递归方法调用太深)

  • 二:如果java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常

java虚拟机栈精简概括:

  1. 每个线程运行时所需要的内存,称为虚拟机栈

  2. 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  3. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

  4. 虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,方法出口

问题辨析:

  1. 垃圾回收是否涉及到虚拟机栈内存?答案:不会涉及,因为进入一个方法时创建一个栈帧,在该方法执行完毕后,出栈后,该栈帧的的局部变量都会被随之释放。
  2. 栈内存空间分配的越大越好吗?答案:不是的,栈空间分配的越大随之线程数也会随之减少,但是也不宜分配的太小,太小会导致无法创建Java虚拟机。
  3. 方法内的局部变量是否线程安全?
    • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

我们来看下一个递归调用出现的StackOverflow异常

  public static void main(String[] args) {
         methodOne();
    }
    public static void methodOne(){
        // 自己调用自己形成递归,没有递归结束条件
        methodOne();
    }

java虚拟机栈的大小是可以进行改变的,设置参数如下:

linux64位的操作系统默认是:1024kb

windows:根据windows的虚拟内存进行分配

-Xss1m
-Xss1024kb    

开发过程中的一些栈内存溢出:如java对象转JSON字符串,Lombok的toString (循环相互依赖,导致递归过深)

使用Gson将java bean转Json字符串(对象直接相互引用)就会出现 StackOverflowError

public class Employee {


   int  id;
   int age;
   String name;
    Department dept;

    public Employee(int id, int age, String name, Department dept) {
        this.id = id;
        this.age = age;
        this.name = name;
        this.dept = dept;
    }

    public Employee() {
    }
}
class Department {

    int depId;
    String depName;
    List<Employee> employees;

    public Department(int depId, String depName, List<Employee> employees) {
        this.depId = depId;
        this.depName = depName;
        this.employees = employees;
    }
    public Department() {

    }
}
class Test{
    public static void main(String[] args) {

        List<Employee> list = new ArrayList<>();
        Department department = new Department(2, "开发", list);

        Employee employee= new Employee(2, 21, "杰克", department);

        list.add(employee);

        Gson gson = new Gson();

          System.out.println( gson.toJson(employee));
          System.out.println( gson.toJson(department));

    }
}

解决方案:不让他们产生循环依赖关系:

public class Employee {

    @Expose
    int  id;
    @Expose
    int age;
    @Expose
    String name;
    @Expose
    Department dept;

    public Employee(int id, int age, String name, Department dept) {
        this.id = id;
        this.age = age;
        this.name = name;
        this.dept = dept;
    }


}
class Department {
    @Expose
    int depId;
    @Expose
    String depName;
    // 在进行转换时,忽略掉该字段
    List<Employee> employees;

    public Department(int depId, String depName, List<Employee> employees) {
        this.depId = depId;
        this.depName = depName;
        this.employees = employees;
    }

}
class Test{
    public static void main(String[] args) {

        List<Employee> list = new ArrayList<>();
        Department department = new Department(2, "开发", list);

        Employee employee= new Employee(2, 21, "杰克", department);

        list.add(employee);

        Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();

          System.out.println( gson.toJson(employee));
          System.out.println( gson.toJson(department));

    }
}

成功进行转换:

image-20211213131610260

CPU占用过高分析定位:

在linux环境下后台运行以下代码:

public class Test {
    public static void main(String[] args) {
      methodOne();
    }
    public static void methodOne(){
       new Thread(()->{
           while (true);
       }).start();
    }

}



  1. javac Test.java
  2. nohup java Test & (以后台启动的方式运行该java程序)
  3. 启动后使用top命令查看系统cpu的一个使用情况,看到java程序 cpu的使用率,直接高达100%

image-20211213134651534

4.我们已经知道进程id,通过 ps H -eo pid,tid,%cpu | grep 5487来查看该进程下有哪些线程 ,可以看到是 5506这个线程

image-20211213134734167

  1. jstack 5487(进程id) 将线程编号转化为十六进制,找到该线程
                                                                                     // 十六进制的线程id
"Thread-0" #11 prio=5 os_prio=0 cpu=33241.74ms elapsed=33.24s tid=0x00007fc0ec1e8000 nid=0x1582 runnable  [0x00007fc0bd4fa000]
   java.lang.Thread.State: RUNNABLE
	at Test.lambda$methodOne$0(Test.java:8)
     // 在我们的第8行代码出现了问题 
	at Test$$Lambda$1/0x0000000100060840.run(Unknown Source)
	at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)

image-20211213135414864

我这里测试完我就直接将其杀死【实际开发中不要直接杀死该进程】:skill -9 5487(进程id)

检测线程死锁:

以下代码就会出现死锁问题

public class Test {

  final static  Object lockA = new Object();
  final static Object lockB = new Object();

    public static void main(String[] args) {

        new Thread(()->{

            synchronized (lockA){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB){
                    System.out.println("Thread-A");
                }
            }
        },"Thread-A").start();

        new Thread(()->{
            synchronized (lockB){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA){
                    System.out.println("Thread-B");
                }
            }
        },"Thread-B").start();
    }

}
  1. 使用 jps -l 指令查看正在运行的java程序,找到我们的Test类
  2. 使用 jstack 进程id 即可查看关系该进程的一个堆栈信息
  3. 关键信息如下:
ava stack information for the threads listed above:
===================================================
"Thread-A":
	at Test.lambda$main$0(Test.java:17)
	- waiting to lock <0x00000000c8a06c48> (a java.lang.Object)
	- locked <0x00000000c8a06c38> (a java.lang.Object)
	at Test$$Lambda$1/0x0000000100060840.run(Unknown Source)
	at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)
"Thread-B":
	at Test.lambda$main$1(Test.java:30)
	- waiting to lock <0x00000000c8a06c38> (a java.lang.Object)
	- locked <0x00000000c8a06c48> (a java.lang.Object)
	at Test$$Lambda$2/0x0000000100061040.run(Unknown Source)
	at java.lang.Thread.run(java.base@11.0.8/Thread.java:834)
// 发现一个死锁
Found 1 deadlock.

2.3 本地方法栈

**本地方法栈(Native Method):**本地方法栈和java虚拟机栈很相似,只不过本地方法栈是为java源代码中带Native关键字修饰的方法所服务的,而虚拟机栈是为那些没有带Native关键字修饰的方法(也就是字节码)所服务的,本地方法栈也会在栈深度溢出或栈扩展失败时抛出StackOverflowError和OutOfMemoryError,Native关键字的方法是看不到的,必须要去oracle官网去下载才源码才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。

就比如说Object类中就有很多Native关键字修饰的方法,不只是Object类,其余的别的类都有Native关键字修饰的方法。

    public final native Class<?> getClass();
    public native int hashCode();
    protected native Object clone() throws CloneNotSupportedException;

2.4 java堆

java堆(heap):

java堆是虚拟机所管理的内存最大的一块区域,java堆被所有的线程所共享的一块内存区域,所以堆中的共享对象需要考虑线程安全问题,java堆在jvm创建的时候分配内存,java堆内存的唯一目的就是存放对象的实例,无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存,《java虚拟机规范》中对堆的描述是:所有的对象以及数组都应该在java堆中分配,java堆是垃圾回收器管理的主要区域,因此也称为GC(Collected Heap)堆。如果在java堆中没有完成实例分配,并且堆无法在进行扩展时,java虚拟机将会抛出 OutOfMemoryError

指定对堆内存分配大小的指令:

-Xmx83886080
-Xmx81920k
-Xmx80m

我们来看下 堆内存溢出的一个代码:

出现异常:Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

public class Test {

    static class OOMObject{

    }
    // 该案例来自:《深入理解java虚拟机3》
    public static void main(String[] args) {
        //  -Xms20m :堆内存的最小值  Xmx20m :堆内存的最大值 (最大值和最小值都为20m避免堆内存自动扩展)
        // -XX:+HeapDumpOnOutOfMemoryError :出现内存溢出时dump出当前内存堆转存储快照便于事后分析
        //添加java虚拟机参数运行以下代码: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
        List<OOMObject> list = new ArrayList<>();
        while (true){
            list.add(new OOMObject());
        }
    }

}

内存诊断:

jps: 查看当前系统中有哪些进程

jmap: 查看堆内存使用情况

// 使用jps
C:\Users>jps
115360 RemoteMavenServer36
215776 Launcher
120980 Test
55260 Jps

// 使用jamp -heap 进程id 查看内存使用情况
C:\Users>jmap -heap 120980
Attaching to process ID 120980, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.251-b08

using thread-local object allocation.
Parallel GC with 10 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 20971520 (20.0MB)
   NewSize                  = 6815744 (6.5MB)
   MaxNewSize               = 6815744 (6.5MB)
   OldSize                  = 14155776 (13.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
 // 新创建的对象都在eden区
Eden Space:
   // Eden 最大容量
   capacity = 5767168 (5.5MB)
   // 已经使用容量    
   used     = 3361488 (3.2057647705078125MB)
   // 剩余容量    
   free     = 2405680 (2.2942352294921875MB)
   58.28663219105113% used
From Space:
   capacity = 524288 (0.5MB)
   used     = 0 (0.0MB)
   free     = 524288 (0.5MB)
   0.0% used
To Space:
   capacity = 524288 (0.5MB)
   used     = 0 (0.0MB)
   free     = 524288 (0.5MB)
   0.0% used
PS Old Generation
   capacity = 14155776 (13.5MB)
   used     = 0 (0.0MB)
   free     = 14155776 (13.5MB)
   0.0% used

3166 interned Strings occupying 259856 bytes.

jconsole: 图形化界面检测工具,可以连续动态检测的工具,多功能检测工具(可以查看内存,线程,类加载信息,jvm参数,Bean在内存中的一个信息)

  • 写个while死循环运行即可,然后连接到你所启动类上

image-20211213151022768

image-20211213151048153

垃圾回收后,内存占用任然很高:

1.先运行以下代码:

public class Test {


    public static void main(String[] args) {
        ArrayList<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
        }
        try {
            // 先睡眠一会儿防止程序停止影响调试
            Thread.sleep(1000*60*60);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
class Student{
    private byte[] bytes = new byte[1024*1024];
}

2.使用jps和jmap查看内存使用情况

C:\>jmap -heap 205940
Attaching to process ID 205940, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.251-b08

using thread-local object allocation.
Parallel GC with 10 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2111832064 (2014.0MB)
   NewSize                  = 44040192 (42.0MB)
   MaxNewSize               = 703594496 (671.0MB)
   OldSize                  = 88080384 (84.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 110100480 (105.0MB)
   // 新生代使用了19.241119384765625MB的大小    
   used     = 20175776 (19.241119384765625MB)
   free     = 89924704 (85.75888061523438MB)
   18.32487560453869% used
From Space:
   capacity = 4718592 (4.5MB)
   used     = 0 (0.0MB)
   free     = 4718592 (4.5MB)
   0.0% used
To Space:
   capacity = 5242880 (5.0MB)
   used     = 0 (0.0MB)
   free     = 5242880 (5.0MB)
   0.0% used
 // 老年代的一个回收情况      
PS Old Generation
   capacity = 341311488 (325.5MB)
   used     = 193653896 (184.68274688720703MB)
   free     = 147657592 (140.81725311279297MB)
   56.73817108669955% used

3153 interned Strings occupying 258984 bytes.

使用jconsole工具连接上执行GC,执行完GC后还是有200多Mb

image-20211213153931269

执行完GC操作后再次查看java虚拟机的一个内存使用情况

using thread-local object allocation.
Parallel GC with 10 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2111832064 (2014.0MB)
   NewSize                  = 44040192 (42.0MB)
   MaxNewSize               = 703594496 (671.0MB)
   OldSize                  = 88080384 (84.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 119537664 (114.0MB)
   // 可以看到新生代确实被回收了一部分 之前的使用情况: used = 20175776 (19.241119384765625MB)   
   used     = 13149392 (12.540237426757812MB)
   free     = 106388272 (101.45976257324219MB)
   11.0002082690858% used
From Space:
   capacity = 5242880 (5.0MB)
   used     = 0 (0.0MB)
   free     = 5242880 (5.0MB)
   0.0% used
To Space:
   capacity = 5242880 (5.0MB)
   used     = 0 (0.0MB)
   free     = 5242880 (5.0MB)
   0.0% used
PS Old Generation
   capacity = 341311488 (325.5MB)
   // 老年代还是未回收掉,  对比以下之前的反而还增加了 : used = 193653896 (184.68274688720703MB)    
   used     = 212095368 (202.26990509033203MB)
   free     = 129216120 (123.23009490966797MB)
   62.14129188643073% used

5602 interned Strings occupying 466440 bytes.

现在使用在控制台输入: jvisualvm 使用jvisualvm 可视化工具来查看

image-20211213154932090

image-20211213155135832

image-20211213155534218

可以看到比较大的一个对象就是我们的ArrayList对象

2.5 方法区

方法区(Method Area):

  • 方法区和堆一样都是多个线程共享的,方法区的数据有线程安全问题,在jvm创建时就分配方法区的一个内存

  • 方法区用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码缓存数据

  • 他的别名是 非堆(Noe heap)当方法区无法满足新的内存分配是会出现 OutOfMemoryError 异常

  • 《java虚拟机规范》对方法区的一个约束是比较宽松的,除了和java堆一样不需要连续的内存和可以选择固定大小或可扩展外,甚至还可以选择不实现垃圾回收

  • 在jdk6的时候就放弃了永久代的概念,到了jdk7已经把原本存放在永久代的字符串常量池,静态变量移出,而到了jdk8完全废弃掉了永久代的一个概念,而是放到了一个元空间(是用的本地内存,也就是操作系统内存,也就是大小是没有上限的)

jdk7及以前方法区大小设置:方法区的大小不是固定的,jvm可以根据应用动态调整

-XX:PermSize 来设置永久代初始化分配空间,默认值是20.75M
-XX:MaxPerSize 来设置永久代最大可分配空间,32位机器默认64M,64位机器默认84M    

jdk8方法区大小设置:方法区的大小不是固定的,jvm可以根据应用动态调整

-XX:MetaspaceSize=?m 来设置永久代初始分配空间,默认值是21m
-XX:MaxMetaspaceSize=?m 来设定永久代最大可分配空间,值为-1(即没有限制)

由于没有上限,因此当本机内存耗尽时,会抛出oom的错误,对于起始值21m来说,如果所使用的内存超过这个值,则会触发full gc卸载没用的类,之后将会重置这个初始值,新的初始值的高低在于gc后释放了多少的空间,释放得少则提升这个初始值,释放得多则降低这个初始值。因此在实际的开发场景中,为了减少full gc的频率,会将这个初始值设置大一些。

jdk8之前会导致永久代内存溢出 :Exception in thread “main” java.lang.OutOfMemoryError: PermGen space

jdk8会在元空间内存溢出: Exception in thread “main” java.lang.OutOfMemoryError: Metaspace

以下过量类加载会导致出现:

 public static void main(String[] args) {
       // 添加虚拟机参数:让方法区的一个大小固定小一点:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
        int j = 0;
        try {
            Test test = new Test();
            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);
        }
    }

常量池:

javap命令的使用:

用法: javap <options> <classes>
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

使用 javap -v Test.class 查看类的详细信息

javap -v Test.class
 // 类文件所在位置   
Classfile /D:/IDE2019/code/thread/target/classes/com/compass/Test.class
  // 时间  
  Last modified 2021-12-13; size 544 bytes
  // 签名    
  MD5 checksum 9405a6e537edaf40357be91adb99d95a
  Compiled from "Test.java"
public class com.compass.Test extends java.lang.ClassLoader
  minor version: 0
  // 版本    
  major version: 52
  // 类的访问修饰符    
  flags: ACC_PUBLIC, ACC_SUPER
 // 常量池      
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/ClassLoader."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/compass/Test
   #6 = Class              #27            // java/lang/ClassLoader
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/compass/Test;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Test.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/compass/Test
  #27 = Utf8               java/lang/ClassLoader
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  // 默认无参构造  
  public com.compass.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/ClassLoader."<init>":()V
         4: return
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/compass/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         // 获取静态变量          
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 加载一个参数         
         3: ldc           #3                  // String hello world
         // 方法调用         
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         // 执行结束         
         8: return
      LineNumberTable:
        line 21: 0
        line 23: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "Test.java"
                                    

常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息

运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址

StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
    • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
    • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份(如果这个对象是对中的复制出来的这个和堆中的那个是不相同的),放入串池, 会把串池中的对象返回

字符串懒加载测试:

public static void main(String[] args) {

        System.out.println(); // 此处时的字符串个数 2471
        System.out.println("0");
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9"); // 此处时的字符串个数 2481
        System.out.println("0");
        System.out.println("1");
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9"); // 此处时的字符串个数 2481(因为字符串常量池中已有,不会重复创建)
        String e4 = new String("Good") + new String("Bye");
        String e5 = "GoodBye";

    }

image-20211213221636515

String 面试题:

图解String面试题地址

public static void main(String[] args) {
        /**
         * 常量池中的信息,都会被加载到运行时常量池,这时常量池中的变量都是常量池中的负号,还没有真正的称为对象
         *  正在执行时 ldc 的时候才会变为正在的字符串对象,准备好一块StringTable(最开始是为null,hash表,不能扩容),
         *  将这个生成好的字符串对象去StringTable中找,没有就添加到StringTable,下次再有相同的字符串就去StringTable中找
         *  并不是一开始就把字符串放入到StringTable,而是执行到该字符串所在位置的时候才会放入到StringTable
         */
        String s1 = "a";
        String s2 = "b";
        /* 字符串拼接会新建一个StringBuilder(线程非安全)对象,然后把两个字符串添加到StringBuilder中去,
         然后调用StringBuilder.toString转String返回,并且添加到StringTable中去 */
        String s3 = "a" + "b";
        // 会创建新的字符串,但不会往常量池中添加
        String s4 = s1 + s2;
        // 在编译期就确定为“ab”会直接从StringTable中去找,如果有就直接拿来用,不会创建新的字符串对象
        String s5 = "ab";
        // 调用intern() 方法时,intern方法会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用
        String s6 = s4.intern();

        // false,s3在常量池,s4在堆中
        System.out.println(s3 == s4);

        // true,s3往常量池中放了一份,s5直接指向常量池中的“ab”
        System.out.println(s3 == s5);

        //  s3指向的是常量池中的“ab”,s6返回的就是常量池中的“ab”
        System.out.println(s3 == s6);

        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        // 因为常量池中已经有“cd”了,x2没能放入到常量池中,所以反回false
        x2.intern();

        System.out.println(x1 == x2);   // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢


    }

StringTable的位置:

image-20211213234514170

验证StringTable所在位置代码:

    public static void main(String[] args) {
        /**
         * -Xmx10m:设置堆的最小和初始大小(以字节为单位)。 必须为1024的整数倍且大于1mb。k或k表示千字节,m或m表示兆字节,g或g表示千兆字节。
         * -XX:-UseGCOverheadLimit:关闭GC回收机制
         */
       // -Xmx5m -XX:-UseGCOverheadLimit

        ArrayList<String> list = new ArrayList<>();
        int i=0;
        try {
            for (int j=0;j<2600000;j++){
                // 往StringTable表中加入数据
                list.add(String.valueOf(i).intern());
                i++;
            }
        }catch (Throwable e){
          e.printStackTrace();
        }finally{
            System.out.println(i);
        }

    }

如果在堆中抛出的是:java.lang.OutOfMemoryError: Java heap space

如果是在永久代::java.lang.OutOfMemoryError: PermGen Space

StringTable 垃圾回收:

 public static void main(String[] args) {
        /**
         * 运行虚拟机参数: -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
         *  -Xmx10m:设置堆内存的最大值
         *  -XX:+PrintStringTableStatistics: 打印StringTable中存放的字符信息
         *  -XX:+PrintGCDetails -verbose:gc :打印垃圾回收的详细信息
         *
         */
        int index=0;
       try {
           for (int i=0;i<10000;i++){
               String.valueOf(i).intern();
           }
       }catch (Throwable e){
           e.printStackTrace();
       }finally {
           System.out.println(index);
       }

    }

控制台打印信息:

// 出现GC
GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->792K(9728K), 0.0022294 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
0
// 堆内存信息    
Heap
 PSYoungGen      total 2560K, used 1210K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 34% used [0x00000000ffd00000,0x00000000ffdb0ad8,0x00000000fff00000)
  from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 288K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 4% used [0x00000000ff600000,0x00000000ff6482f8,0x00000000ffd00000)
 Metaspace       used 3243K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
  // 类字节码符号表(类名,方法名,变量名等...)                             
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13306 =    319344 bytes, avg  24.000
Number of literals      :     13306 =    568688 bytes, avg  42.739
Total footprint         :           =   1048120 bytes
Average bucket size     :     0.665
Variance of bucket size :     0.666
Std. dev. of bucket size:     0.816
Maximum bucket size     :         6
// 字符串常量池信息(StringTable是由哈希表来的实现的 数组+链表)                               
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000 // 数组的大小
Number of entries       :     11681 =    280344 bytes, avg  24.000 // 字符串对象个数
Number of literals      :     11681 =    634304 bytes, avg  54.302 // 字符串常量
Total footprint         :           =   1394752 bytes   // 占用的字节数
Average bucket size     :     0.195
Variance of bucket size :     0.209
Std. dev. of bucket size:     0.457
Maximum bucket size     :         4

StringTable调优:

    /**
     * 虚拟机参数:
     *  -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics (让StringTable的大小变大些)
     *  -XX:+PrintStringTableStatistics (不设置,系统默认)
     *  -XX:StringTableSize=1009 -XX:+PrintStringTableStatistics (让StringTable的大小变小些)
     *
    *  调优的思路:
    StringTable是基于数组加链表的:数组的长度越大,那么产生哈希碰撞的几率就越小,也就是我们的链表更短,查询速度更快
    * 如果StringTable太小,那么尝试哈希碰撞的几率就会很高,导致我们的链表很长,每次出现新字符串的时候都要去哈希表中去做比对,
     *  那么查询效率肯定就慢 注意StringTable的大小在  1009 ~ 2305843009213693951 之间超过这个范围虚拟机无法创建,
     *  尽量计算出一个合适的大小,不然设置的太大,既然是个数组,那就得初始化内存,浪费系统资源,需要适当调整StringTable的大         小,不是直接搞个最大
     */
    public static void main(String[] args) throws IOException {
        // 搞一个里面装有字符的文本即可,尽量文本多一些
        FileInputStream fileInputStream = new FileInputStream("src/linux.words");
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
        String line;
        long start = System.nanoTime();
        while (true){
           line=bufferedReader.readLine();
            if (line!=null) line.intern();
            if (line==null)break;
           
        }
        // 最终结果是毫秒
        System.out.println("cost="+(System.nanoTime()-start)/1000000);
    }

小总结:如果项目中使用了大量的字符串,并且字符串中大量的值是相等的,那在进行使用这些字符串的时候,调用 String::itnern() 先进行一个入池操作,减少堆内存的一个使用。

2.6 直接内存

直接内存(Direct Meonry): 直接内存并不是java虚拟机的一个运行时数据区域,也不是《java虚拟机规范》中定义的内存区域,但是这部分也是被频繁使用,而且也可能会出现 OutOfMemoryError 在jdk1.4中加入了Nio类 有的类就是使用的直接内存,他可以调用Native方法直接分配堆外内存空间,通过一个存储在DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些常见下显著的提高性能,因为避免了java堆和Native堆来回复制数据

直接内存溢出案例: Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory

static int  memorySize = 1024*1024*1024;
    public static void main(String[] args) throws IOException {
        ArrayList<ByteBuffer> list = new ArrayList<>();
        int index=0;
        try {
            while (true){
                // 创建1G的直接内存放入到ArrayList中去,避免被GC
                ByteBuffer buffer = ByteBuffer.allocateDirect(memorySize);
                list.add(buffer);
                index++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            System.out.println(index);
        }

    }

直接内存也是可以被回收的:

程序没有停止之前:

image-20211214023928019

程序停止之后:可以看到直接就被回收掉了,不会出现内存泄漏的情况

image-20211214024041977

具体看下直接内存是怎么分配和释放的:在DirectByteBuffer的构造方法中有个 unsafe.allocateMemory(size)就是分配直接内存的

    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            // 该方法就是用来进行分配内存的
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
           
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // Cleaner是一个虚引用类型,当他锁关联的这个对象被垃圾回收器回收时Cleaner就会触发他的clear()方法
        // Deallocator 实现了Runnable接口,自己就是一个任务对象
        cleaner = Cleaner.create(this (this就是DirectByteBuffer) , new Deallocator(base, size, cap));
        att = null;


    }

 public void clean() {
        if (remove(this)) {
            try {
                // 由一个referenceHandler线程进行调用
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
  // 如果虚引用关联的对象(DirectBuffer)被回收,referenceHandler线程就会调用这个run方法进行回收直接内存 
  public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 释放掉直接内存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
  • 使用了 Unsafe 对象完成直接内存的分配回收,调用 unsafe.allocateMemory(size)来分配内存,并且回收需要主动调用 unsafe.freeMemory(address);方法释放直接内存
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内。

显示垃圾回收堆直接内存的影响:

有时候我们会使用 -XX:+disableExplicitGC虚拟机参数来让显示的GC无效,也就是在调用System.gc()的时候无效,只有等真正内存占用比较多,java虚拟机自动GC的时候才会垃圾回收。这样就导致了我们的直接内存迟迟得不到释放。

解决的方法就是:是反射机制,拿到Unsafe对象,自己手动调用unsafe.freememory()方法来释放直接内存。

2.7 对象内存布局

对象在内存中存储的布局可以分为3块区域:对象头实例数据对齐填充

对于第三部分的对齐填充并不是必然存在的,也没有特别的含义,仅仅是为了占位而已,因为HotSpot虚拟机的字段内存管理系统要求对象的起始地址必须是8字节的整数倍,如果不满足8的整数倍,就需要填充占位符。

32位虚拟机mark word 信息如下:

image-20211215235351768

64位虚拟机下 Mark Word 对象头信息如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rviIygUL-1666016850029)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20211215235217394.png)]

Class word:类型指针,即对象执行他的类型元数据的指针,java虚拟机通过这个指针来确定该对象是那个类的实例。如果对象是一个java数组,那么还有一部分长度是用来存储数组的长度的。这个 array length 是32位 由此我们可以得出数组的最大长度为 2^31-1也就是Integer的最大值。实例数据部分是真正存储有效信息的,也就是我们代码中定义的各种字段。

2.8 对象的访问定位

我们都知道我们栈中的变量名,存储的仅仅是对象的引用(Reference)而已,通过这个引用来操作我们堆中的对象,由于在《java虚拟机规范》中并没有规定这个引用是以什么方式去定位的,对象的访问模式也是由java虚拟机锁规定的。主流的方向有两种:

  • 句柄访问:如果使用句柄访问的方式,java的堆中会划分一块儿内存来作为句柄池,Reference中存储的就是句柄地址,而句柄中包含了对象的实例数据和类型数据各自具体的地址信息。好处就是:Reference中存储的是稳定的句柄地址在对象被移动时,只会改变句柄中实例数据的指针。
  • 直接访问:如果使用直接指针进行访问的话,java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,因为Reference中直接存储的就是对象的地址。好处就是:如果只是访问对象本身的话,就不必多次间接引用的开销。我们的Hotspot虚拟机就是使用的这种直接访问的方式。

文字描述起来非常的抽象,我们来看两张图(来自深入java虚拟机3)

image-20211216002948758

image-20220308212650149

3.对象回收时机

3.1 垃圾回收概述

  • GC(Carbage Collection )主要用于Java堆的管理。Java中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。

  • 在早期1960年诞生麻省理工学院的Lisp是第一门使用内存动态分配和垃圾回收的术语。当时的Lisp还在胚胎期,其作者John MacCarthy就在思考垃圾回收需要完成的三件事情:

  1. 那些内存需要回收?
  2. 什么时候回收
  3. 如何回收?

我们已经了解了jvm运行时数据区域,其中程序计数器,虚拟机栈,本地方法栈随线程而生,随线程而结束,每一个栈帧中分配多少内存,基本在类结构确定下来时就是已知的(尽管在运行期和即时编译器会进行一些优化,在在基于概念模型的讨论下大体上认为编译期是可知的)因此这几个区域的内存都具有确定性,在这几个区域内不需要过多考虑垃圾回收问题,当方法结束调用时,内存自然跟着回收。

java堆内存中几乎存在所有java对象的实例,垃圾回收器回收之前就需要判断那些对象是存活的,那些对象是已经死了(该对象没有任何用途了,需要被释放掉,不再占用堆内存)

3.2 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用他时,计数器加一,当引用失效时,计数器减一,任何时刻引用计数器为零的对象是不能再被使用的

引用计数器算法((Reference Counting)),实现简单,判断效率也很高,例如之前微软的COM(Component Object Model)现在的Python技术语言都使用了引用计数器算法。那既然引用计数器算法简单又好用,我们的Java是不是用了引用计数器算法呢?我告诉你:不是的,我们先来看一段代码思考一下?执行 testGC() 判断我们的objA和objB能不能被垃圾回收器锁回收?

public class Test  {

    public Object instances = null;
    private static final  int _1MB = 1024*1024;
    // 这个bigSize就是占用一点内存看看是否被垃圾回收器锁回收
    private  byte[] bigSize =new byte[_1MB]; 
    public static void main(String[] args) throws IOException {
        
        new Test().testGC();

    }
    
    public void testGC(){
        Test objA = new Test();
        Test objB = new Test();

        objA.instances=objB;
        objB.instances=objA;

        objA=null;
        objB=null;
        // 假设这里发生GC objA和objB是否能被回收掉?
        System.gc();
    }

}

至于java虚拟机没有用引用计数器的原因就是会造成对象直接相互循环引用,导致计数器一直不能减一的操作,那么这个对象也就永远得不到回收,就例如上面这个例子,虽然objA和objB两个对象毫无用处,但是由于他们的instances都相互引用这对方,导致引用计数器一直不为零,引用计数器算法也就对他无效。

3.3 可达性分析算法

当前的(java,c#,上面提到的Lisp)都是通过可达性分析算法来管理内存子系统的。这个算法的基本思路就是通过一个 GC Roots 的根对象作为起始节点集,从这些 GC Roots节点开始根据引用关系向下进行搜索,搜索的一个路径叫做 引用链 ,如果某个对象到 GC Roots没有任何引用链相连,证明此对象不可能再被引用,下面看一张图,如图所示:虽然object5,object6,object7虽然相互关联,但是他们到 GC Roots 是不可达的,所以将会被判定为可回收的对象。

image-20211214155936899

在java技术体系里面可以作为 GC Roots 的对象包括如下几种:

  • 在虚拟机栈(占中的局部变量表)中引用的对象,比如:各个线程被调用的方法堆中使用到的参数,局部变量,临时变量等
  • 在方法区中类静态属性引用的对象,比如java类的引用类型变量
  • 在方法区中常量引用的对象,比如字符串常量池(String Table)里的引用
  • 在本地方法栈中引用的对象
  • java虚拟机内部的引用,比如基本数据类型对应的Class对象,以下常驻的异常对象(NullPointException,OutOfMemoryError),还有类加载器
  • 所有被同步锁持有的对象(Synchorized)持有的对象
  • 反映java虚拟机内部情况的 JMXBean,JVMTI中注册的回调,本地代码缓存等。

除了这些固定的 GC Roots 外,还有别的一些根据用户所选的垃圾回收器,以及当前内存回收区域不同,还可以有其他的对象临时性加入 ,比如后面会提到的分代收集和局部回收,为了避免GC Roots 包含的对象过多而膨胀,不同的垃圾收集器也会做不同的优化处理,这点我们后面在介绍。

3.4再谈引用

image-20211214191924517

  • 强引用(StronglyReference)通过 new 关键出来的对象都是强引用,只要还有引用关系在,无论任何情况都无法被垃圾回收器所回收
  • 软引用(SoftReference) 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象 可以配合引用队列来释放软引用自身 ,java中的软引用类【SoftReference】
  • 弱引用(WeakReference) 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身,java中的软引用类 【WeakReference】
  • 虚引用(PhantomReference) 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法 [Cleaner的clean() ] 释放直接内存,java中的虚引用类【PhantomReference】
  • 终结器引用(FinalReference) 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象
  • java中的引用队列:ReferenceQueue

软引用的一个案例使用:我们创建一个集合通过这样的一个引用关系: list->softReference->byte[] 来观察他们的一个运行情况

 // 虚拟机参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc
    private static final  int _5MB=1024*1024*10;
    public static void main(String[] args) throws IOException {
          soft(_5MB);
        //strong(_5MB);


    }
    // 使用软引用:当堆内存空间不够执行垃圾回收,这时候回收完如果内存还是不够,再发起一次GC回收掉软引用
    public static void soft(int size){
        // list->softReference->byte[]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[size]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        System.out.println("循环结束...");
        for ( SoftReference<byte[]> reference: list){
            System.out.println(reference.get());
        }

    }

    // 使用强引用,即使堆内存不够引用关系还在,也不会触发GC 直接抛出:java.lang.OutOfMemoryError: Java heap space ,
    public static void strong(int size) {
        List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            byte[] bytes = new byte[size];
            System.out.println(bytes);
            list.add(bytes);
            System.out.println(list.size());
        }

        System.out.println("循环结束...");
        for ( byte[] content: list){
            System.out.println(content);
        }
    }

案例提升:虽然byte[]数组被释放掉了但是我们的软引用还没有被释放掉,我们配合ReferenceQueue将软引用也彻底释放掉

 // 使用软引用:当堆内存空间不够执行垃圾回收,这时候回收完如果内存还是不够,再发起一次GC回收掉软引用,把软引用也清除掉
    public static void softImprove(int size){

        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 使用引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
            // 关联了软引用队列,当软引用关联的byte[]对象被回收时,软引用自己会被加入到ReferenceQueue中去
            SoftReference<byte[]> ref = new SoftReference<byte[]>(new byte[size],queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        System.out.println("循环结束...");
        // 获取队列中最先放入的元素,从队列中移除掉

        Reference<? extends byte[]> firstReference = queue.poll();
        while (firstReference!=null){
            // 从队列中获取无用的软引用对象,并从list集合中移除掉那些软引用对象
            list.remove(firstReference);
            firstReference=queue.poll();
        }
        System.out.println("----------------------------------------");
        for (SoftReference<byte[]> softReference : list) {
            System.out.println(softReference.get());
        }

    }

4.Hotspot算法细节实现

4.1 根节点枚举

我们在判断对象是否消亡时,使用GC Roots 的方式来进行查找,尽管我们的查找目标很明确,但是要在查找过程中做到高效并非是一件容易的事情。现在的应用庞大,光是方法区的大小常有数百上千兆,若要逐个检查以这里为起源的引用肯定得消耗不少时间。所有根节点在枚举这一步骤用户线程都是需要停止的。整理内存碎片一样的会暂停用户线程(stop the workd),现在可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发,,但是根节点枚举必须在一个能保证一致性的快照中才可以得以进行。这里的一致性指的是执行子系统看起来像是停止在某个时间点,不会出现在分析过程中,因为根节点的对象引用关系还在不断的变化,若是这点不能满足,那么分析结果的正确性就无法保障。这就是导致收集过程中必须暂停所有用户线程的原因之一。即使是号称停顿时间可控的,几乎不会发送停顿的 CMS,G1,ZGC等垃圾回收器,枚举根节点时,也是必须停止的。

由于目前主流java虚拟机使用的都是准确式垃圾收集器,当用户线程停下来后,并不需要一个不漏的检查完所有执行上下文的全局引用位置。虚拟机应当是有方法知道那些地方还存着引用。Hotspot的解决方案里,使用的是一组OoMap的数据结构来达到这个目的,一旦类加载动作完成的时候,Hotspot会把对象内什么偏移量上是什么类型的数据计算出来,也会在特定的位置记录下栈里和寄存器里那些位置是引用,这样收集器在扫描时,就可以得知这些信息,并不需要一个不漏的从方法区等GC Roots 开始查找。

4.2安全点

在OoMap的协助下,Hotspot 可以快速的完成Gc Roots节点的枚举,但又引起了新的问题,可能导致引用关系的变化,或者说OoMap内容变化的指令非常多,如果对每个指令都生成一个新的OoMap,那么将需要大量额外的存储空间,这样的垃圾收集器空间成本就非常高。

实际上Hotspot 也没有为每条指令都生成一个OoMap,上面已经提到在特定的位置记录了这些信息,这些位置 称为 安全点,有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停下来开始垃圾回收,而是强制必须执行到安全点才能够暂停,安全点的选调不能太少以至于让收集器等的时间太久,也不能太频繁以至于过分增加运行时的内存负荷,安全点的位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的。因为每条指令的执行时间都非常短,程序不太可能因为指令流太长这样一直长时间执行,长时间执行最明显的特征就是指令序列的复用,例如方法跳转,循环跳转,异常跳转都属于指令序列复用,所有具有这些功能的指令才会产生安全点。

如何让垃圾收集发生时,让所有的线程(这里不包含执行JNI调用的线程)都跑到安全点,然后停顿下来?这里有两种解决方案:

  • 抢占式中断(Preemptive Suspension): 抢占式中断不需要线程的执行代码主动去配合,在垃圾回收发生时,系统先把所有线程中断,如果发现有用户线程不在安全点上,就恢复这条线程,让他一会儿重新中断,直到跑到安全点上,现在几乎没有虚拟机采用这种抢占式中断。
  • 主动式中断(Voluntary Suspension): 主动式中断的思想是当垃圾回收需要中断线程时,不对线程直接操作,仅仅是简单的设置一个标志位,各个线程执行的时候不停的主动去轮询这个标志,一旦发小标志位真时自己就在最近的安全点上主动中断挂起。Hotspot使用内存保护陷阱的方式,把轮询操作精简至一条汇编指令。

4.3 安全区域

使用安全点的方式完美解决了如何停止用户线程,让虚拟机进入垃圾回收状态的问题,但是实际情况却不一定,安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾回收过程的安全点,但是在程序不执行的时候呢?所谓的不执行就是没有分配处理时间,比如用户线程处于sleep或Blocked状态,这时候线程就无法响应虚拟机的中断请求,不可能在走到安全点中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理时间,对于这种情况,就必须引入安全区域(Safe Region)的方式来解决。

安全区域是指能保证在某一段代码片段中,引用关系不会发生改变,因此在这个区域任意地方开始收集垃圾都是安全的,也可以把安全区看做是被延伸扩展了的安全点。当用户线程执行到安全区中的代码是,首先会标识自己已经进入安全区域,那样这段时间里虚拟机要发起来讲回收时,就不必去管这些已经声明自己在安全区内的线程了,当线程需要离开安全区时,它要检查虚拟机是否已经完成根节点枚举或垃圾收集过程中其他需要暂停用户线程的阶段,如果完成线程就当做声明事情都没有发生继续执行,否则一直等待,直到收到可以离开安全区域的信号为止。

4.4 记忆集与卡表

**对象的跨代引用:**就是对年轻代执行Minor GC的时候,但是新生代的对象完全有可能被老年代所引用,(新生代里面的对象是老年代对象中的成员变量),为了找出老年代的对象,不得不再去额外的遍历整个老年代中的所有对象来确保可达性分析结果的正确性,遍历整个老年代虽然这个方案可以,但是这样无疑是为内存回收带来了很大的负担。对应这样的第二个解决方案就是:在新生代上建立一个数据结构(该数据结构就是 记忆集,Remembered Set),这个结构把老年代划分成多个小块儿,标识出老年代的那一块儿内存会存在跨代引用,以后发生Minor GC时,只有包含了跨代引用的小块儿内存里的对象才会被加入到GC Roots中进行扫描。

image-20211217011100727

事实上并不是老年代和新生代才会有这样的问题,所有设计到部分区域收集的垃圾回收器都会面临相同的问题,典型的如:G1,ZGC和Shenandoah收集器,因此我们需要进一步清理记忆集合实现方式。记忆集是一组用于从非收集区域指向收集区域的指针集合的抽象数据结构,我们不考虑成本的话,可以使用非收集区域中所有含跨代引用的对象数组来实现。

以指针对象来实现记忆集的伪代码:

public class RememberedSet {
 Object [] set[OBJECT_INTERGENERATIONAL_REFERENCE_SIZE];
}

这种记录全部含跨代引用对象的实现方案,无论是从维护成本和内存占用都相当高。而在垃圾回收场景中,收集器只需要通过记忆集判断某一块儿非收集区域是否存在有指向了收集区域的指针就可以了,并不需要这些跨代指针的具体全部实现细节,那在设计记忆集的时候,可以选择更为粗狂的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围外的)精度。

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字段包含跨代指针
  • 对象精度:记录精确到一个对象,该对象含有字段和跨代指针
  • 卡精度:记录精确到一块儿内存区域,对象里含有跨代指针

**卡精度:**所指的是一种称为 卡表(Card Table) 的方式去实现记忆集的,这也是目前最常用的记忆集实现形式,记忆集只是一种抽象的数据结构,并不是具体的实现,卡表就是记忆集的一种具体实现,他定义了记录精度,与堆内存的映射关系等,记忆集与卡表的关系就好比是java中的接口(记忆集)和实现类(卡表)。卡表最简单的形式可以是一个字节数组,而Hotspot也是这样实现的。

字节数组 cardTable[ ] 每一个元素都对应其标识内存区域中一块儿指定大小的内存,这个内存称为 卡页(card page)一般来说卡页的大小都是2n次幂的字节数,一个卡页中通常包含不止一个对象,只要卡页中有一个或多个对象字段存在的跨代指针,那就将对卡表数组的元素值标识为1,称之这个元素变脏(Dirty),没有就标识为0,在垃圾收集发生时,只需要筛选出卡表中变脏的元素,将他们加入到GC Roots中一并扫描即可

image-20211217010959196

4.5 写屏障

我们解决了如何使用记忆集来缩减 GC Roots 的扫描范围,但是还没有解决卡表元素如何维护的问题,例如他们什么时候变为脏?,谁来把他们变脏?

卡表中的元素变脏的时机是很明确的——有其他部分区域中对象引用了本区域对象时,对应卡表元素就应该变脏,变脏的时间点就是应该发生在引用类型字段赋值的那一刻,但问题是如何变脏?即如何在对象赋值的那一刻去更新卡表的维护呢?加入是解释执行的字节码,那相对好处理,虚拟机赋值对每条字节码指令的执行,有充分的介入空间,但在编译执行场景当中呢?经过即时编译器后的代码已纯粹是机器指令流了,这就必须在机器码层面的手段,把维护卡表的动作放到每一个赋值操作中去。

Hotspot里是通过写屏障(write Barrier)技术来维护卡表状态的,这里的写屏障并不是并发编程中的写屏障和读屏障,不要将两者进行混淆。写屏障可以看成是虚拟机层面对引用类型字段赋值 这个动作的AOP切面,在引用对象赋值时,会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的范围内,在赋值前的屏障叫做:写前屏障,在赋值后的屏障,叫做:写后屏障,Hotspot虚拟机许多垃圾收集器都有用到写屏障,但直至G1之前其他收集器都只用到了写后屏障。应用写屏障后,虚拟机会为所有赋值操作生成相应的指令,一旦收集器在写屏障中更新了卡表操作,无论更新是不是老年代的对象引用,每次只要执行更新,就会操作额外的开销,不过这比Minor GC时扫描整个老年代的开销要小得多。

除了写屏障的开销外,在高并发的场景下,还会产生伪共享的情况。现代中央处理器以缓存行为存储单位,当多线程修改相互独立的变量时,如果这些变量刚好共享同一个缓存行,就会彼此影响(写回,无效或同步化)。为了避免伪共享问题:一种简单的解决方案就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其变为脏。

在jdk7后,Hotspot虚拟机新增了一个参数, -XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断,开启后会增加一次额外的判断开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据实际运行情况来权衡测试。

4.6 并发的可达性分析

在之前的内容中提到了当前主流编程语言的垃圾回收器基本上都是通过可达性分析算法来判断对象是否存活,可达性分析算法理论要求全程都基于一个能保证一致性的快照中才能进行分析,这就意味着必须暂停所有的用户线程运行,在根节点枚举这个步骤中,由于GC Roots 相比起整个java堆中全部的对象比较还是算少数的,且在各种优化手段上(OoMap)的加持下,他停顿的时间非常短,相对于比较固定(不随堆容量而增长)可从 GC Roots 在继续往下遍历对象图这一步骤的停顿时间必定就会与堆容量的增长成正比关系:堆越大,存储的对象越多,对图结构越复杂,需要标记更多对象的停顿时间自然增加。

要知道包含 标记 的阶段是所有追踪式垃圾回收算法的共同特征,如果这个阶段随着堆容量的变大而增加停顿时间,其影响会波及所有的垃圾回收器,如果能够减少这部分停顿时间,那么收益也是系统性的。

想解决或降低用户线程停顿,就先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象的图遍历?为了解释清除这个问题,,我们引入三色标记作为工具来辅导,把遍历过程中遇到的对象,按照是否访问过这个条件标记为三种颜色。

  • 白色:表示尚未被垃圾回收器所访问过,显然在可达性分析刚刚开始的时候,所有对象都是白色的,如果在可达性分析阶段结束后,该对象还是白色的,即代表不可达。
  • 黑色: 表示该对象已被垃圾回收器所访问过,且这个对象的所有引用都扫描过,它是安全存活的如果有其他对象引用直接指向了黑色对象,无需进行重新扫描(因为某个对象不可能不经历灰色阶段就指向某个白色对象)
  • 灰色:表示该对象已经被垃圾回收器访问过,但这个对象至少存在还有一个引用没有被扫描过

收集器在修改对象图标记颜色的过程中,用户线程在修改引用关系,这样就会出现两个问题。

一:把原本消亡的对象标记为存活,这显然不是好事,但起始可以容忍,只不过产生了点浮动垃圾,下次垃圾收集的使用清理掉即可。

二:把原本存活的对象标记为消亡:这种就是非常致命的了,程序肯定会因此而发生不可预知的错误。

对象消失示意图:

image-20211217154347874

当这两个条件满足时,会发生对象消失的问题,即原本应该是黑色的对象被误标记为白色:

  • 赋值器插入了一条或多条从白色对象到黑色对象的新引用
  • 赋值器删除了全部从灰色到该白色对象的直接引用或间接引用

两种解决方案:

  • 增量更新:增量更新破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就需要把新插入的引用记录下来,等并发扫描结束后,再将这些记录过引用关系中的黑色对象为跟,重新扫描一次。
  • 原始快照:破坏第二个条件,当灰色对象要删除指向白色的对象的引用关系时,就将要删除的这个引用记录下来,并发扫描结束后,在将这些记录过引用关系中的灰色对象为跟,重新扫描一次,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照进行搜索。
  • 在Hotspot虚拟机中,CMS基于增量更新来做并发标记,G1和Shenandoah则是用原始快照来实现。

5.垃圾回收算法

从如何判断对象消亡的角度来说,垃圾收集算法分为两大类:引用计数式垃圾收集器和追踪式垃圾收集,也被称为直接垃圾收集和间接垃圾收集

分代收集理论: 当前商业的垃圾收集器都遵循了分代收集器的一个规范。

强分代假说:绝大多数对象都是朝生夕灭的。

弱分代假说:熬过越多次垃圾回收过程的对象越难消亡。

这两个分代加深奠定了多款垃圾收集器的一致性设计原则:收集器应该将java堆划分出不同的区域,然后根据对象的年龄分配到不同的区域之中存储。如果一个区域中的对象存活时间都比较短,把他们集中在一个区域内,每次都只关心那些存活的对象,而不是耗大量的精力去关系那些对象要消亡,就能以比较低的代价回收回收大量空间,如果剩下的对象都难以消亡,那就把它集中到一个指定的区域,虚拟机可以使用比较低的频率来回收这个区域,这就是同时兼顾了垃圾收集时间的开销和内存空间的有效使用

4.1 标记清除算法

image-20211214193202816

标记清除算法(Mark Sweep): 这个算法正和他的,名字一样。分为标记和清除两个阶段,首先标记出那些需要回收的对象,统一回收被标记的对象,也可以反过来:标记那些存活的对象,统一回收未被标记的对象。标记清除算法是最基础的收集算法,后续的算法都是根据他进行优化的,标记清除算法实现起来感觉比较容易,但是他却有两个缺点:

  • 一:执行效率不问题,如果java堆中有大量的对象,而且大部分对象是需要回收的,这时必须进行标记和清除的动作,标记和清除的效率都会随java堆中的对象增多而降低
  • 二:内存空间碎片化的问题,标记和清除后会产生大量内存不连续的内存碎片,碎片太多会导致程序在运行过程中突然要存储一个比较大的对象时,无法找到一块内存连续的空间,不得不提前触发一次垃圾回收动作,现象如下图所示:

image-20211214204620273

4.2 标记复制算法

**标记复制算法(Mark Copy):**标记清除算法会留下许多的碎片空间,导致内存地址不连续,标记复制算法就是为了解决这个问题,1960年Fenichel提出了一种叫半区复制的概念,将可用内存划分为相等的两块空间,每次只使用其中一块儿,如果其中一块儿的内存使用完毕了,就将存活的对象复制到另一块儿内存区域,,这种算法会产生大量的内存间复制开销,但对于大多数对象都是可回收的,算法只需要复制少量的对象到另一块儿内存空间,而且每次都是真的半区内存回收,分配内存时也不用考虑空间碎片化的复杂情况,只需要移动堆顶指针按顺序分配即可,这种方式实现简单,运行高效,但是可用内存空间直接会减半

image-20211214211042904

补充一点:新生代中98%的对象是熬不过第一轮的,所以也就不用分配1:1的一个比例,在1989年,Andrew Appel针对这种生存期比较短的对象提出了基于标记复制算法的优化,先称之为 Appel式回收   HotSpot 虚拟机的 Serial,ParNew 等新生代收集器均采用了这种策略来设计新的内存布局。Appel式回收主要做的事情就是:把新生代的区域分为一块儿比较大的Eden和两块比较小的Survivor,每次分配内存时,只使用Eden和其中一块儿Survivor,发送垃圾回收时,将Eden和另一块儿Survivor中存活的对象复制到另外一块儿Survivor上,直接清空使用过的Enden和Survivor,HotSpot 虚拟机默认Eden和Survivor的比例是8:1,新生代的可使用空间就是90%,还有剩余10%就是那块儿Survivor。

4.3 标记整理算法

标记整理算法(Mark Compact):标记复制算法在对象的存活率比较高的时候,就要进行多次复制,效率会降低,更关键的是,如果不想浪费50%的空间,就需要额外的空间进行担保,以应付内存中对象都100%存活的现象,所以在老年代是不能采用这种算法的。标记清除算法和标记整理算法都是差不多的,但是标记清除算法是非移动式的,原来的对象还是在原来的位置,而标记整理算法会在清除之后做一个移动操作,将存活的对象紧挨着放,这样就不会留下碎片空间,但是新问题又来了,移动存活对象并更新所有对象的引用是一种极为负重的操作,而这种对象移动操作会暂停用户应用程序才会能进行。CMS收集器就是采用的标记清除算法,先暂时忍耐内存碎片化的存在,直到内存空间的碎片化已经影响到对象分配时,采用标记整理算法收集一次,以获得规整的内存空间,CMS收集器面临空间碎片过多时就是采用这种方法进行解决的

image-20211214214226905

6.分代垃圾回收

新生代、老年代、永久代(方法区)的区别

  1. Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
  2. 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。
  3. 在jdk1.6就放弃了永久代的概念,1jdk.8完全将永久代里面存放的字符串常量池,静态变量等移入到元空间(本地内存)
  4. 老年代就一个区域,新生代分为三个区域,我们在讲标记复制算法的时候也有提到,也就是一个Eden和From Survivor和To Survivor
  5. 默认的,新生代与老年代 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
  6. 新生代默认的,Eden : From Survivor : To Survivor = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,From Survivor = To Survivor = 1/10 的新生代空间大小。
  7. 永久代(元空间)就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。

为什么要这样进行年龄分代?

主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法:

  • 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

  • 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

新生代又分为Eden和 From Survivor ,To Surivor 三个区。加上老年代就这四个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代【大对象是指需要大量连续内存空间的java对象】)。当Eden没有足够空间的时候就会触发jvm发起一次Minor GC,将Eden区域的数据复制到 To Survivor中,交换To Survivor 和From Survivor的位置,。如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。

分代收集的基本流程:

当Eden空间不足时:先标记出需要回收的对象

image-20211215023048327

复制出存活对象和清除需要回收的对象

image-20211215023728319

Survivor区域的对象经过15次回收,还是没有回收掉,那么就会进入到老年代。(不同的垃圾收集器,有不同的实现不一定必须达到15次),假设这次的Eden区域又满了。

image-20211215024548331

再次清除和复制之后就是这样。

image-20211215025050475

假设o2现在经过15次回收还是没有被回收掉,

image-20211215030541086

如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC。

image-20211215032237659

Minor GC、Major GC、Full GC区别及触发条件

Minor GC:

  • eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。
  • 新创建的对象大小 > Eden所剩空间
  • **补充:**Minor GC时,应用需要挂起,也就是 stop the world:暂停其他用户线程,让垃圾回收线程先工作,垃圾回收工作完成再让用户线程恢复运行,暂停时间比较短,因为回收的大部分是需要回收的对象,复制的对象相对于比较少,最大寿命是15(4bit)分代年龄包含在java的对象头中(mark word)

Major GC:

  • Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。
  • 补充: Major GC时,应用需要挂起,也就是 stop the world:暂停其他用户线程,让垃圾回收线程先工作,垃圾回收工作完成再让用户线程恢复运行,相对于Minor GC暂停时间相对比较长,老年代采用的标记清除算法和标记整理算法,前面也有提到过。

Full GC:

  • Full GC是清理整个堆空间,包括年轻代和老年代

6.1 JVM的一些相关参数

参数描述
-Xms堆初始大小
-Xmx 或 -XX:MaxHeapSize=size堆最大大小
-Xmn 或 (-XX:NewSize=size(新生代初始大小) + -XX:MaxNewSize=size(新生代最大大小) )新生代大小
-XX:InitialSurvivorRatio=ratio(默认8指的是Edne的大小) 和 -XX:+UseAdaptiveSizePolicy(后期动态调整)Survivor比例(动态)
-XX:SurvivorRatio=ratioSurvivor比例
-XX:MaxTenuringThreshold=threshold晋升老年代阈值
-XX:+PrintTenuringDistribution晋升详情
-XX:+PrintGCDetails -verbose:gcGC详情
-XX:+ScavengeBeforeFullGCFullGC 前先执行 MinorGC

6.2 GC分析

先新建一个main方法,不添加任何代码,在运行前添加如下虚拟机参数:

         /**
         * 虚拟机参数:-Xms20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
         * Xms20M:初始堆空间
         * Xmn10M:新生代空间
         * XX:+UseSerialGC:使用垃圾回收器SerialGC(Survivor区域不会动态调整)
         * -XX:+PrintGCDetails -verbose:gc:打印GC详细信息
         */

GC信息解读:

// 堆中的详细信息
Heap
 // 新生代大约是9M ,user:使用大约2M【类加载花费的内存】
 def new generation   total 9216K, used 2663K [0x0000000082200000, 0x0000000082c00000, 0x0000000082c00000)
  // Eden 分到大约8M                                             
  eden space 8192K,  32% used [0x0000000082200000, 0x0000000082499f10, 0x0000000082a00000)
  // From Survivor 1M                             
  from space 1024K,   0% used [0x0000000082a00000, 0x0000000082a00000, 0x0000000082b00000)
  // TO Survivor 1M                                  
  to   space 1024K,   0% used [0x0000000082b00000, 0x0000000082b00000, 0x0000000082c00000)
 tenured generation   total 10240K, used 0K [0x0000000082c00000, 0x0000000083600000, 0x0000000100000000)
   the space 10240K,   0% used [0x0000000082c00000, 0x0000000082c00000, 0x0000000082c00200, 0x0000000083600000)
 Metaspace       used 3153K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K

运行以下代码:分析GC信息

 // 虚拟机参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc
    private static final  int _512KB =1024*512;
    private static final  int _1MB = 1024*1024;
    private static final  int _6MB = 1024*1024*6;
    private static final  int _7MB = 1024*1024*7;
    private static final  int _8MB = 1024*1024*8;
    public static void main(String[] args) throws IOException {
        /**
         * 虚拟机参数:-Xms20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
         * Xms20M:初始堆空间
         * Xmn10M:新生代空间
         * XX:+UseSerialGC:使用垃圾回收器SerialGC(Survivor区域不会动态调整)
         * -XX:+PrintGCDetails -verbose:gc:打印GC详细信息
         */
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);

    }

image-20211215051911666


[GC (Allocation Failure) [DefNew: 2499K->761K(9216K), 0.0020058 secs] 2499K->761K(19456K), 0.0030301 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
Heap
 //新生代 总共容量:216K,已经使用容量:8339K   
 def new generation   total 9216K, used 8339K [0x0000000082200000, 0x0000000082c00000, 0x0000000082c00000)
  eden space 8192K,  92% used [0x0000000082200000, 0x0000000082966830, 0x0000000082a00000)
  from space 1024K,  74% used [0x0000000082b00000, 0x0000000082bbe680, 0x0000000082c00000)
  to   space 1024K,   0% used [0x0000000082a00000, 0x0000000082a00000, 0x0000000082b00000)
 // 老年代 总共容量:10240K   已经使用容量:0K                         
 tenured generation   total 10240K, used 0K [0x0000000082c00000, 0x0000000083600000, 0x0000000100000000)
   the space 10240K,   0% used [0x0000000082c00000, 0x0000000082c00000, 0x0000000082c00200, 0x0000000083600000)
 // 元空间                               
 Metaspace       used 3241K, capacity 4496K, committed 4864K, reserved 1056768K
 // 类加载的信息               
 class space    used 351K, capacity 388K, committed 512K, reserved 1048576K
// used:是分配给所有类加载器的metachunk中用来存储元数据的部分的大小
// capacity:分配给所有类加载器的metachunk的大小,包含每个metachunk的head、used、wasted和current metachunk的free部分
// committed:向OS申请的内存中已经分配物理内存的大小,包含VirtualSpaceList中所有node已经commit的部分(也就是非当前node+当前node commit的部分),或者说所有类加载器的capacity+全局空闲链表Chunk Manager+硬件预留HWM margin
// reserved:JVM进程虚拟地址空间的部分,像class metaspace默认CompressedClassSpaceSize是1GB,所以reserved就是1GB

别的线程抛出OutOfMemoryError主线程会不会停止运行呢?

public class Test  {
    private static final  int _8MB = 1024*1024*8;
    public static void main(String[] args) throws IOException, InterruptedException {
        /**
         * 虚拟机参数:-Xms20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
         * Xms20M:初始堆空间
         * Xmn10M:新生代空间
         * XX:+UseSerialGC:使用垃圾回收器SerialGC(Survivor区域不会动态调整)
         * -XX:+PrintGCDetails -verbose:gc:打印GC详细信息
         */

        // 我的my-Thread线程抛出:java.lang.OutOfMemoryError: Java heap space,并不影响我主线程
        new Thread(()->{
         try {
             ArrayList<byte[]> list = new ArrayList<>();
             for (int i=0;i<1000;i++){
                 list.add(new byte[_8MB]);
             }
         }catch (Throwable e){
             e.printStackTrace();
         }
        },"my-Thread").start();

        Thread.sleep(1000*60);
    }
}

当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行

6.3 垃圾回收器的分类

image-20211215185739319

先来看下响应能力和吞吐量的一个概念:

吞吐量:吞吐量关注的是,在一个指定的时间内,最大化一个应用的工作量。

  • 在一定时间内同一个事务(或者任务、请求)完成的次数(tps)
  • 数据库一定时间以完成多少次查询
  • 对于关注吞吐量的系统,一定次数卡顿(即stw)是可以接受的,因为这个系统关注长时间的大量任务的执行能力,单次快速的响应并不值得考虑

响应能力:响应能力指一个程序或者系统对请求是否能够及时响应,比如:

  • 一个桌面UI能多快地响应一个事件

  • 一个网站能够多快返回一个页面请求

  • 数据库能够多快返回查询的数据

  • 串行:单线程的垃圾回收器,在来垃圾回收发生时,暂停其他的线程,适用场景:堆内存比较小,cpu核数少。

  • 吞吐量优先:多线程的垃圾回收器,适合堆内存比较大,多核cpu来支持,让单位时间内stw的时间最短,垃圾回收时间越低,吞吐量越高。

  • 响应时间优先:多线程的垃圾回收器,适合堆内存比较大,多核cpu来支持,尽可能让单次stw的时间最短。

6.3 .1 串行GC

开启串行垃圾回收器:-XX:+UseSerialGC=Serial+SerialOld

Serial: 在新生代,采用标记复制算法
**SerialOld:**工作在老年代,采用标记整理算法

在垃圾的回收过程中,对象的地址会发生改变,为了保证不被别的线程所干扰,用户线程需要在一个安全点进行暂停下来,等到垃圾回收工作完成,在让用户线程恢复运行。

image-20211215160647541

6.3 .2 吞吐量优先GC

 // 这两个参数默认在jdk1.8开启,UseParallelGC(标记复制算法):工作在年轻代,工作在老年代:UseParallelOldGC(标记整理算法),只需要开启其中一个,另外一个也会开启
        // 垃圾回收线程的个数默认是和cpu核数相关的
        -XX:+UseParallelGC -XX:+UseParallelOldGC
        // ParallelGC自适应调整新生代的Eden和Survivor的比例和堆内存大小,晋升阈值
        -XX:+UseAdaptiveSizePolicy
        // 调整垃圾回收时间和总时间的占比,ratio默认值99,公式:1/(1+ratio)
        -XX:GCTimeRatio=ratio
        // 最大stw的最大暂停数,默认200ms
        -XX:MaxGCPauseMillis=ms
        // 控制ParallelGC的线程数
        -XX:ParallelGCThreads=n

image-20211215161744278

6.3 .1 响应时间优先GC

  /*
       基于表标记清除算法的并发垃圾收集器,CMS垃圾回收器在垃圾回收的情况下用户线程能继续运行,也需要stw,但是停止的时间较短,工作在老年代的垃圾回收器,与NewGC配合使用,NewGC工作在新生代基于标记复制算法的垃圾回收器,在并发失败的情况,从CMS垃圾回收器退化到SerialOld单线程的垃圾回收器,初始标记只会标记Roots对象
         XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
         // ParallelGCThreads:并行的GC线程数,和CPU数量向关联,ConcGCThreads:并发的GC线程数,一般这个参数设置为并行线程数的四分之一
        -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
        // 执行CMS垃圾回收的占比,设置为80,老年代的内存使用率达到80%的时候就执行CMS垃圾回收,为的是清除浮动垃圾
        -XX:CMSInitiatingOccupancyFraction=percent
        // 重新标记的阶段,此时新生代有些即将消亡但还有引用老年代的对象导致老年代在回收之前做一些无用的查找工作,该参数就是在执行重新标记之前做一次垃圾回收,减少重新标记时的压力
        -XX:+CMSScavengeBeforeRemar
        */

6.4HotSpot 虚拟机中的GC

在了解这些收集器之前,我们先了解下,串行,并行,并发这三个概念:

串行:在执行时间上不可能重叠,只有上一个任务执行完,才可以接着执行下一个任务

并行:在执行时间上是重叠运行的,多个任务在同一时间上执行,互不干扰

并发:在执行时间上不可能同一时间运行,在同一个时间点只有一个任务在运行,多个任务可以相互干扰,交替执行****

6.4.1 Serial

Serial收集器是最基本的、历史最悠久的收集器,曾经是JDK 1.3.1之前虚拟机的新生代收集的唯一选择。Serial这个名字揭示了这是一个单线程的垃圾收集器,特点如下:

  • 仅仅使用一个线程完成垃圾收集工作;单线程的
  • 在垃圾收集时必须暂停其他所有的工作线程,知道垃圾收集结束;
  • Stop the World是在用户不可见的情况下执行的,会造成某些应用响应变慢;
  • 使用复制算法;

Serial收集器的工作流程如下图:

image-20211217170256549

虽然如此,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。它的优点同样明显:简单而高效(单个线程相比),并且由于没有线程交互的开销,专心做垃圾收集自然课获得最高的单线程效率。在一般情况下,垃圾收集造成的停顿时间可以控制在几十毫秒甚至一百多毫秒以内,还是可以接受的。

6.4.2 Serial Old

image-20211217170217728

Serial Old是Serial的老年版本,在Serial的工作流程图中可以看到,Serial Old收集器也是一个单线程收集器,使用“标记-整理”算法。这个收集器主要给Client模式下的虚拟机使用。如果在Serve模式下,它有两个用途:一个是在JDK 1.5之前的版本中与Parallel Scavenge收集器搭配使用;另一个就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这个收集器的工作流程在Serial的后半部分有所体现。

6.4.3 ParNew

ParNew收集器其实是Serial收集器的多线程版本,与Serial不同的地方就是在垃圾收集过程中使用多个线程,剩下的所有行为包括控制参数、收集算法、Stop the World、对象分配规则和回收策略等都一样。ParNew收集器也使用复制算法。ParNew收集器的工作流程如下:

image-20211217170127936

ParNew收集器看似没有多大的创新之处,但却是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为,除了Serial收集器外,目前只有ParNew收集器能够与CMS收集器配合工作,而CMS收集器是HotSpot在JDK 1.5时期推出的具有划时代意义的垃圾收集器。

ParNew收集器在单个线程的情况下由于线程交互的开销没有Serial收集器的效果好。不过,随着CPU个数的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

ParNew的一些相关参数:

//年轻代中Eden区与两个Survivor区的比值.注意Survivor区有两个.如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:SurvivorRatio=n
    
//选择垃圾收集器为并行收集器。此配置仅对年轻代有效。可以同时并行多个垃圾收集线程,但此时用户线程必须停止。  
-XX:+UseParallelGC
    
//设置年轻代为多线程收集。可与CMS收集同时使用。在serial基础上实现的多线程收集器
-XX:+UseParNewGC

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

    

6.4.4 Parallel Old

Parallel Old收集器是Parallel Scavenge收集器的老年版本,它也使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6开始提供。

在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器的组合。Parallel Old收集器的工作流程如下:

image-20211217170025583

6.4.5 Parallel Scavenge

Parallel Scavenge收集器和ParNew类似,是一个新生代收集器,使用复制算法,又是并行的多线程收集器。不过和ParNew不同的是,Parallel Scavenge收集器的关注点不同。

CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)。如果虚拟机一共运行100分钟,垃圾收集运行了1分钟,那么吞吐量就是99%。

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

Parallel Scavenge收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能在给定时间内完成垃圾收集。不过垃圾收集时间的缩短是以牺牲吞吐量和新生代空间为代价的,短的垃圾收集时间会导致更加频繁的垃圾收集行为,从而导致吞吐量的降低。

GCTimeRatio参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如果设置为19,那允许的最大GC时间就是总时间的5%(1/(1+19))。默认是99,也就是允许最大1%的垃圾收集时间。

Parallel Scavenge收集器也叫吞吐量优先收集器,它还有一个参数-XX:UseAdaptiveSizePolicy,这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量,这叫GC自适应的调节策略。这也是Parallel Scavenge收集器和ParNew收集器的一个重要区别。

6.4.6 CMS

MS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。在重视响应速度和用户体验的应用中,CMS应用很多,在java 的web开发场景需要响应速度比较高,希望系统停顿时间段,给用户带来比较好的交互体验,CMS收集器就比较符合这类场景。

CMS收集器使用“标记-清除”算法,运作过程比较复杂,分为4个步骤:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS Concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS Concurrent Sweep)

其中,初始标记和并发标记仍然需要Stop the World、初始标记:仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记:就是进行GC RootsTracing 的过程,重新标记:则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段长,但远比并发标记的时间短。并发清除:清理掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以和用户线程并发,初始标记和并发标记都是比较快的,比较慢的是重新标记

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以整体上说,CMS收集器的内存回收过程是与用户线程一共并发执行的。下图是流程图:

image-20211217172759733

CMS的优点就是并发收集、低停顿,是一款优秀的收集器。不过,CMS也有缺点,如下:

  • CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU个数大于4时,垃圾收集线程使用不少于25%的CPU资源,当CPU个数不足时,CMS对用户程序的影响很大;
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure” 而导致另一次Full GC;
  • CMS使用标记-清除算法,会产生内存碎片,导致老年代即使有足够大的空间但是不连续,导致分配大对象的时候失败,导致提前Full GC;
  • 在堆内存比较小时用CMS优势稍微比较好,在堆内存比较大时用G1(6GB-8GB)优势稍微比较好

6.4.7 Garbage First

相关 JVM 参数

  • - XX:+UseG1GC
  • - XX:G1HeapRegionSize=size
  • - XX:MaxGCPauseMillis=tim

G1(Garbage first)Garbage first开创了收集器面向局部收集的设计思想和Region的内存布局形式,G1收集器是最先进的收集器之一,是面向服务端的垃圾收集器。

G1收集器作为CMS收集器的替代者,设计者们的目标是希望建能够建立起“停顿时间模型(Pause prediction Model)”的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集时间大概率不超过N毫秒的模板。在G1之前出现的垃圾收集器,包括CMS在内,垃圾收集的范围要么是整个年轻代( Minor GC),整个老年代(Major GC),整个java堆(Full GC),而G1收集器跳出了这个规则,他可以面向堆内存的任何部分来组成收集(Collection Set 简称:CSt),衡量的标准不在是它属于那个年代,而是那块内存的垃圾存放的多,回收益大,这就是G1的 Mixed GC的模式。

G1虽然遵循分代理论进行设计,但是堆内存的设计布局与其他的垃圾收集器大不相同,G1把连续的java堆分为多个大小相等的 Region 每一个 Region 可以根据需要进行扮演 新生代的 Eden和Survivor空间,收集器对于不同Region 扮演的不同角色采用不同的策略进行处理。

G1中还有个比较特殊的区域,就是Humongous 区域,专门用来存储大对象,G1认为只要超过 一个Region一半区域的对象就是大对象,每个Region区域的大小通过参数:-XX:G1GeapRegionSize 设定,取值范围1MB~32MB,且应该为2的N次方幂,而那些超过 Region区域的超大对象就会存储到 Humongous Region区域中去,而G1大多数时候都将 Humongous Region当成是老年代来进行看待。

G1的单次回收最小单元是Region,因此每次回收到的内存空间都是Region的整数倍,这样可以避免在java堆中安全区域进行垃圾回收,更具体的处理思路是:让G1收集器去跟踪各个Region里面垃圾堆里“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先列表,每次根据用户指定的收集停顿时间(使用参数:-XX:MaxGCPauseMillis 指定,默认值是200毫秒)来收集。优先处理回收价值比较大的Region区域。

G1收集器与其他收集器相比,G1收集器有如下优点:

  • 并行与并发: G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。

  • 分代收集:G1 收集器与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。

  • 空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC 。

  • 可预测的停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。

  • G1整体上是 标记+整理 算法,两个区域(Region)之间是 复制 算法

G1中也有分代的概念,不过使用G1收集器时,Java堆的内存布局与其他收集器有很大的差别,它将整个Java堆划分为多个大小相等的独立区域(Region),G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次优先收集价值最大的那个Region。这样就保证了在有限的时间内尽可能提高效率。

G1收集器的大致步骤如下:

  1. 初始标记(Initial Marking):仅仅只标记GC Roots能直接关联到的对象,并且修改TANS指针的值,让下一阶段用户线程并发运行时能正确的可用Region中新分配的对象,停顿线程,但耗时很短。

  2. 并发标记(Concurrent Marking):从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行,当对象图扫描完成后还需要处理SATB记录下的并发时有引用变动的对象。

  3. 最终标记(Final Marking):修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录

  4. 筛选回收(Live Data Counting and Evacuation):这里有一个预估,对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,与用户程序一起并发执行,暂停用户线程

收集器的流程如下图:

image-20211217190211739

G1如何解决跨代引用?
Region每个区域都有自己的记忆集,这些记忆集会记录别的Region指针指向自己,并标记这些指针属于那个卡页的范围之内,G1的记忆集存储结构上实际上是一张Hash表,key存放的是Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号,因为每个Region都有自己的记忆集相比于其他的垃圾收集器的记忆集,G1有着更高的占用内存负担,估计要耗费java堆容量10%~20%的堆容量来维护收集器的工作。

G1如何在并发标记阶段保证收集线程和用户线程的互不干扰?
G1收集器采用的是原始快照(SATB)算法的方式来实现的,垃圾收集的过程中还会影响到用户线程新创建的对象在内存上分配。G1为每个Region区域设计了两个TAMS(Top at Mark Start)的指针,把Region一部分空间划分出来,用于并发回收的过程中新对象的分配,并发回收时新分配的对象地址必须在这两个指针位置上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认存活。如果内存回收的速度赶不上内存分配的速度,G1垃圾收集器也会被迫冻结用户线程,导致Full GC 而产生长时间的 Stop the world。

G1 垃圾回收阶段

image-20211215175117638

Young Collection:

image-20211215175541572

新生代内存紧张将Eden的数据copy到Survivor:

image-20211215180429190

Survivor中的数据超过阈值,晋升到老年代

image-20211215181157389

  • Young Collection + Concurrent Mark 在 Young GC 时会进行 GC Root 的初始标记

  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

  • - XX:InitiatingHeapOccupancyPercent=percent (默认45%)

image-20211215181954397

Mixed Collection
会对 Eden、Survivor、Old 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW
  • -XX:MaxGCPauseMillis=ms

image-20211215183404460

6.4.8 Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
    • 补充:串行的垃圾收集器有两种: Serial与Serial Old,一般两者搭配使用。新生代采用Serial,利用复制算法;老年代使用Serial Old采用标记-整理算法。串行的这种垃圾回收器适合Client端模式。
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足:CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure” 而导致另一次Full GC, CMS使用标记-清除算法,会产生内存碎片,导致老年代即使有足够大的空间但是不连续,导致分配大对象的时候失败,导致提前Full GC
  • Garbage First
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足 :当垃圾回收的速度赶不上垃圾产生的速度,会发生oncurrent mode failure 老年代的垃圾收集器从CMS退化为Serial Old,所有应用线程被暂停,停顿时间变长。触发Full GC

6.4.9 jdk8u20 字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
  • 开启参数:-XX:+UseStringDeduplication 默认开启
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}

  • 所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,s1和s2这两个对象引用指向的都是同一个char[] value 数组
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表

6.4.10 JDK 8u40 并发标记类卸载

  • 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸 载它所加载的所有类

  • 参数 -XX:+ClassUnloadingWithConcurrentMark 默认启用

6.4.11 JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间
    • 当整个堆占用超过某个百分比时,就会触发并发GC周期,这个百分比默认是45%(jdk8),我的理解来说,如果你的项目没有大的cpu负载压力,可以适当降低这个值,带来的好处就是提前开始Concurrent Marking Cycle Phases ,进一步来说,回收 年轻代 and 老年代 也会提前开始,这样有利于防止年轻代晋升老年代失败(老年代容量不足)而触发Full GC

7.GC调优(Hotspot)

7.1确定目标

查看虚拟机运行参数:

java -XX:+PrintFlagsFinal -version | findstr "GC"

调优的考虑:科学计算(高吞吐量),web开发(响应优选),根据目标选择合适的垃圾回收器

高吞吐量:CMS,G1,ZGC

响应优先:ParallelGC

5.2 最快的GC是不发生GC

是不是代码写的有问题,加载了一些不必要的数据,导致内存不够用,频繁GC

查看FullGC 前后的内存占用,考虑如下几个问题

  • 是不是数据太多? 如果执行SQL查询的时候:select * from TableName(大表);尽量筛选出有用数据,不用加载不必要的数据
  • 数据表是不是太臃肿?
    • 对象图(表连接太多,查询出不相关联的数据)
    • 对象的大小,java中最小的Object都需要占用16个字节,Integer 24 ,int 4 是不是可以使用基本数据类型替换包装数据类型
  • 是否存在内存泄漏?static Map map 存放了许多对象,然后不移除出。使用,软引用,弱引用进行包装,java不适合做缓存用第三方工具

5.3 新生代调优

新生代的特点

  • 所有的 new 操作的内存分配非常廉价
    • 当new一个对象时在Eden中进行分配,每个线程都在Eden中分配一个私有的区域TLAB(thread-local allocation buffer),每个线程私有的,局部的缓冲区,每次分配时都会检测TLAB中是否有内存,如果有优先在这个区域进行分配,对象分配的过程中也有安全问题,因此在做对象分配时,也要做线程安全的保护,由JVM帮我们做线程安全保护。
  • 死亡对象的回收代价是零,新生代发生垃圾回收,都是采用复制算法。
  • 新生代大部分对象用过即死,只有少数存活下来
  • Minor GC 的时间远远低于 Full GC,Full GC 的代价比较大,相差1~2个数量级。

新生代空间分配的越大越好么?

  • 新生代设置的太小,一旦分配对象时,新生代的空间不足就会触发Minor GC,就会造成 stop the world
  • 新生代的空间设置的太大,老年代的空间太小,新生代就不会触发Minor GC,新生代的对象进入老年代区域内存不够就会触发 Full GC,Full GC的stop the world 的时间比Minor GC的时间更久,得不偿失。新生代内存建议是 整个堆内存 25%~50%。

调优思路1: 新生代的容量>=并发量*请求响应的数据大小 ,因为一次请求响应的过程,大部分的对象都会被回收,因为这样能避免触发MinorGC,如果一次请求响应的数据超过新生代的容量,那么就有可能触发MinorGC,更严重的是会触发FullGC。

  • 设置大对象直接进入老年代 大对象就是需要大量内存空间的对象(比如数组、字符串) ,通过jvm参数-XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。

    具体操作:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC
    使用场景:当我们可以确定系统中的对象大部分为大对象,且短期内不会被垃圾回收,就可以根据对象大小设置jvm参数,让这些大对象直接进入老年代,省去了对象在新生代流转的过程(因为新生代大部分采用的都是复制算法,复制来复制去就浪费了系统资源),节省了Eden区的空间,因为大对象最终总会进入老年代的,还不如提前让出Eden空间,让他处理更多的小对象,提升系统性能!

  • 置长期存活的对象提前进入老年代! 新生代的对象每熬过一次Minor GC ,其年龄就会+1,默认15岁,也就是流转15次就会进入老年代。当我们的系统中大概有大部分(80%)的对象都会经过15次Minor GC 进入老年代,我们可以通过设置-XX:MaxTenuringThreshold 来调整进入老年代需要的年龄阈值。比如设置年龄为8即可进入老年代,这样那些长期存活的对象,就可以尽早的进入老年代,减少对象在新生代的流转次数,提升了系统性能!

  • 根据survivor区的动态年龄判断机制,合理设置新生代大小 。一般超过survivor区大小的60%会发生动态年龄判断机制,此时把最老的对象放进老年代。可以适当增加survivor区的大小避免Full GC!动态年龄判断的机制作用其实是希望那些可能是长期存活的对象,尽早进入老年代,避免多次复制操作而降低效率。

  • -XX:+PrintTenuringDistribution : 每次在垃圾回时把Survivor去中对象的详细信息显示出来

调优思路2:幸存区大小=当前活跃对象+待晋升老年代的对象, 如果幸存区比较小,jvm就会自动调整晋升阈值,导致新生代的一些对象被提前晋升到老年代,变向的延长了对象的生存时间,直到老年代的空间不足触发FullGC

5.4 老年代调优

以 CMS 为例:

  • CMS 的老年代内存越大越好,因为采用的是标记清除算法,会导致内存空间碎片化,导致老年代内存空间不足,Concurrent Mode Failure从而退化到 Serial Old串行垃圾收集器,从而效率大幅度降低。
  • 先尝试不做调优,如果没有 Full GC 说明系统运行比较正常,如果经常发生FullGC 那先试着调年轻代的大小,Eden大小,晋升阈值等,如果年轻代调优还是不能解决问题,再回过头调优老年代,观察是多大的内存导致FullGC发生,在原有基础上进行增加1/4 ~ 1/3
  • 老年代的空间使用率达n%进行CMS回收:-XX:CMSInitiatingOccupancyFraction=percent (比例越低,老年代触发垃圾回收的时机就越早),一般设置75%~80%

5.5 调优案例

  • 案例1:FullGC 和 Minor GC频繁

可能是新生代的空间不足,导致对象的晋升阈值被动态调整,老年代存储了大量的存活期比较短的对象,导致老年代频繁FullGC

  • 案例2:请求高峰期发送FullGC,单次暂停时间比较长(CMS)

在重新标记这个阶段,会扫描年轻代和老年代,在重新标记之前,先做一次年轻代的回收,减少新生代的对象数量,在重新标记的时候扫描的对象少些。参数: -XX:CMSScavengBeforeRemark

  • 案例3:老年代比较充裕的情况下发送FullGC(CMS jdk1.7)

djk7采用的是永久代,永久代的空间不足也会导致FullGC,调整永久代的初始容量和最大容量即可

8.类加载和字节码技术

Class本质和数据类型

如何解读class文件的二进制字节码文件呢?

  • 方式1:使用 EditPlus.exe,选择以16进制的形式进行显示
  • 方式2:使用javap命令: javap -v  -p fileName.class
  • 方式3:使用idea插件 jclasslib BytecodeViewer
  • jvm描述文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

javap详细用法:

javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

Class类的本质:

任何一个Class文件都对应一个类或接口文件的定义信息,但相反过来,Class文件他并不一定以磁盘文件的形式存在,Class文件是一组以8位自己为基础的二进制流

Class文件的格式:

Class文件不像XML等描述语言,他没有任何分隔符,所以在其中的数据项,无论是字节顺序还是数量,都定义的非常严格,那个字节是什么含义,长度是多少?先后顺序如何》都不允许被改变。

Class 文件格式采用一种类似C语言结构体的方式来存储数据,这种结构体中只有两种数据:无符号

  • 无符号数属于基本数据类型。u1,u2,u4,u8来分别代表:1个字节,2个字节,4个字节,8个字节的无符号数,无符号数可以用来描述数字,索引引用,数值,或按照UTF-8的编码构成字符串。
  • 表是由多个符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性的以_info结尾,用于描述关系复杂的数据结构,整个Class实际上就是一张表,由于没有长度限制,通常会在前面加个数来说明。

8.1类文件结构

image-20211218124605154

Class一个简单的 HelloWorld程序文件的结构并不是一成不变的,随之java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但基本是非常稳定的。

image-20211218165317889

字节码结构:

类型名称说明长度数量
u4magic识别Class文件4byte1
u2minor_version服版本号2byte1
u2major_version主版本号2byte1
u2constant_pool_count常量池计数器2byte1
cp_infoconstant_pool[constant_pool_count-1]常量池表N byteconstant_pool_count-1
u2access_flags访问标识2byte1
u2this_class类索引2byte1
u2super_class父类索引2byte1
u2interfaces_count接口计数器2byte1
u2interfaces[interfaces_count]接口索引集合2byteinterfaces_count
u2fields_count字段计数器2byte1
filed_infofields[fields_count]字段表N bytefields_count
u2methods_count方法计数器2byte1
method_infomethods[methods_count]方法表N bytemethods_count
u2attributes_count属性计数器2byte1
attribute_infoattributes[attributes_count]属性表N byteattributes_count
public class ClassLoad {
    public static void main(String[] args) {

    }
    private int num=1;
    public int add(){
        num=num+2;
        return num;
    }
}

在linux系统下以16进制的内容显示该文件就可以得到如下内容:xxd ClassLOad

linux下查看二进制文件
以十六进制格式输出:
od [选项] 文件
od -d 文件 十进制输出
-o 文件 八进制输出
-x 文件 十六进制输出
xxd 文件 输出十六进制

在vi命令状态下:
:%!xxd :%!od 将当前文本转化为16进制格式
:%!xxd -c 12 每行显示12个字节
:%!xxd -r 将当前文本转化回文本格式

1个字节=2个16进制字符,一个16进制位=0.5个字节。

CA FE BA BE 00 00 00 34  00 1A 0A 00 04 00 16 09
00 03 00 17 07 00 18 07  00 19 01 00 03 6E 75 6D
01 00 01 49 01 00 06 3C  69 6E 69 74 3E 01 00 03
28 29 56 01 00 04 43 6F  64 65 01 00 0F 4C 69 6E
65 4E 75 6D 62 65 72 54  61 62 6C 65 01 00 12 4C
6F 63 61 6C 56 61 72 69  61 62 6C 65 54 61 62 6C
65 01 00 04 74 68 69 73  01 00 1F 4C 63 6F 6D 2F
63 6F 6D 70 61 73 73 2F  67 65 6E 65 72 61 6C 2F
43 6C 61 73 73 4C 6F 61  64 3B 01 00 04 6D 61 69
6E 01 00 16 28 5B 4C 6A  61 76 61 2F 6C 61 6E 67
2F 53 74 72 69 6E 67 3B  29 56 01 00 04 61 72 67
73 01 00 13 5B 4C 6A 61  76 61 2F 6C 61 6E 67 2F
53 74 72 69 6E 67 3B 01  00 03 61 64 64 01 00 03
28 29 49 01 00 0A 53 6F  75 72 63 65 46 69 6C 65
01 00 0E 43 6C 61 73 73  4C 6F 61 64 2E 6A 61 76
61 0C 00 07 00 08 0C 00  05 00 06 01 00 1D 63 6F
6D 2F 63 6F 6D 70 61 73  73 2F 67 65 6E 65 72 61
6C 2F 43 6C 61 73 73 4C  6F 61 64 01 00 10 6A 61
76 61 2F 6C 61 6E 67 2F  4F 62 6A 65 63 74 00 21
00 03 00 04 00 00 00 01  00 02 00 05 00 06 00 00
00 03 00 01 00 07 00 08  00 01 00 09 00 00 00 38
00 02 00 01 00 00 00 0A  2A B7 00 01 2A 04 B5 00
02 B1 00 00 00 02 00 0A  00 00 00 0A 00 02 00 00
00 0B 00 04 00 0F 00 0B  00 00 00 0C 00 01 00 00
00 0A 00 0C 00 0D 00 00  00 09 00 0E 00 0F 00 01
00 09 00 00 00 2B 00 00  00 01 00 00 00 01 B1 00
00 00 02 00 0A 00 00 00  06 00 01 00 00 00 0E 00
0B 00 00 00 0C 00 01 00  00 00 01 00 10 00 11 00
00 00 01 00 12 00 13 00  01 00 09 00 00 00 3D 00
03 00 01 00 00 00 0F 2A  2A B4 00 02 05 60 B5 00
02 2A B4 00 02 AC 00 00  00 02 00 0A 00 00 00 0A
00 02 00 00 00 11 00 0A  00 12 00 0B 00 00 00 0C
00 01 00 00 00 0F 00 0C  00 0D 00 00 00 01 00 14
00 00 00 02 00 15 

8.1.1 魔数

前4个字节表示是否是java类型的class类型文件

8.1.2 版本

4~7 字节,表示类的版本 00 34(十进制52) 表示是 Java 8,次版本在这里没有体现,版本兼容,jdk版本向下兼容

我们需要注意的是开发环境中的jdk版本和线上环境的jdk版本是否一致

虚拟机jdk颁布为1.k(k>=2)时,对应的Class文件格式的版本范围为:45.0-44+k.0(含两端)

jdk版本主版本(10进制)
JDK 1.145
JDK 1.246
JDK 1.347
JDK 1.448
JDK 1.549
JDK 1.650
JDK 1.751
JDK 1.852

8.1.3 常量池表

Constant TypeValue描述
CONSTANT_Class7类或接口的符号引用
CONSTANT_Fieldref9字段的符号引用
CONSTANT_Methodref10类中方法的符号引用
CONSTANT_InterfaceMethodref11接口中方法的符号引用
CONSTANT_String8字符串类型字面量
CONSTANT_Integer3整形字面量
CONSTANT_Float4浮点字面量
CONSTANT_Long5长整形字面量
CONSTANT_Double6双精度浮点字面量
CONSTANT_NameAndType12字段或方法的符号引用
CONSTANT_Utf81UTF-8编码的字符串
CONSTANT_MethodHandle15表示方法句柄
CONSTANT_MethodType16标志方法类型
CONSTANT_InvokeDynamic18表示一个动态方法调用点

在版本号之后跟的是常量池的数量。已经若干个常量池表项。常量池计数器从1开始而不是从0开始。常量池表项中用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池。常量池计数器就是用于记录常量池中有多少项。

为了满足后面某些常量指向常量池的索引值的数据在特定情况下需要表达 不引用常量池中的任何一项,这种情况索引值用0来表示

字面量和符号引用

  • 字面量
    • 文本字符串
    • 声明为final的常量值
  • 符号引用
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

全限定名: com/jvm.corez这就是全限定名,把全类名的.替换为了 /,在最后使用 ; 号表示结束

**字段:**简单名称是指没有类型和参数修饰的方法或字段名称,比如例子中的add()num变量

描述符: 描述符的作用就是描述字段的数据类型,方法的参数列表,(包括数量,类型,顺序),返回值

标志描述
B基本数据类型 byte
C基本数据类型 char
D基本数据类型 double
F基本数据类型 float
I基本数据类型 int
J基本数据类型 long
S基本数据类型 short
Z基本数据类类型 boolean
V表示 void 没有返回值
L对象类型 eg: Ljava/lang/Object;
[数组类型,代表一维数组 Eg: [L@5caf905d;

**补充:**符号引用和直接引用的区别

1.符号引用:

  • 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

2.直接引用:

  • 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  • 一个能间接定位到目标的句柄
         // 直接引用
        String a = new String("hello");
        //符号引用
        String b = "hello";

类常量结构图

image-20211218220624431[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-za1aESSM-1666016850041)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20211218221253074.png)]

小总结:

  • 这14中表的共同特点就是,表开始的第一位都是u1标识位tag,代表当前这个常量使用的是那种表结构,即那种数据类型
  • 在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。
  • 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。

为什么常量池中药包含这些内容?

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

8.1.4 访问标识

在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为 public类型;是否定义为 abstract类型,如果是类的话,是否被声明为final等。各种访问标记如下所示:

image-20211218221836457

  • 类的访问权限通常为ACC_开头的常量。
  • 每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是public final的类则该标记为ACC_PUBLIC | ACC_FINAL。
  • 使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。
  1. 带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。
  • 如果一个class文件被设置了ACC_INTERFACE标志,那么同时也得设置AcC_ABSTRACT标志。同时它不能再设置 AcC_FIAL,ACC_SUPER或ACC_ENUM标志。
  • 如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除 Acc_ANNOTATTON外的其他所有标志
    当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
  1. ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java SE 8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。
    • ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的 ACC_SUPER标志在由DK 1.0.2之前的编译器所生成的access _flags中是没有确定含义的,如果设置了该标志,那么Oracle的Java虚拟机实现会将其忽略。
  2. Acc_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
  3. 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
  4. ACC_ENUM标志表明该类或其父类为枚举类型。
  5. 表中没有使用的access_flags标志是为未来扩充而预留的,这些预留的标志在编译器中应该设置为0,Java虚拟机实现也应该忽略它们。

8.4.5 类索引,父类索引,接口索引集合

在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:

image-20211218223321111

这三项数据来确定这个类的继承关系。

  • 类索引用于确定这个类的全限定名
  • 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了
    java.lang.0bject 之外,所有的Java类都有父类,因此除了java.lang.0bject外,所有Java类的父类索引都不为0。
  • 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

this_class(类索引)

  • 2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如com/jvm/demo。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。

8.4.6 字段表集合

fileds:

  • 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。

注意事项:

  • 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  • 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节
    码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

fileds[] (字段表):

  • fields表中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。·一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。
    • 作用域( public、 private、protected修饰符)
    • 是实例变量还是类变量(static修饰符)
    • 可变性(final)
    • 并发可见性(volatile修饰符,是否强制从主内存读弘>可否序列化(transient修饰符)
    • 字段数据类型(基本数据类型、对象、数组)Ⅰ>字段名称
    • 字段表结构

字段表作为一个表,同样有他自己的结构:

image-20211219162159077

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

**描述符索引:**描述符的作用是用来描述字段的数据类型、方法的参数列表〈包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double ,float,int,long, short , boolean’及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示,如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fTaG9V1J-1666016850043)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20211218232206862.png)]

**属性表集合:**一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count中,属性具体内容存放在attributes数组中。

image-20211218233059677

8.4.7 方法表

**methods:**指向常量池索引集合,它完整描述了每个方法的签名。

  • 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public,private或protected),方法的返回值类型以及方法的参数信息等。
  • 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。
  • 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法()和实例初始化方法())

注意事项

  • 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此3ava语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。
  • 也就是说,尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和ava语法规范相反,字节码文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。

**methods_count(方法计数器): ** methods_count 的值表示当前class文件methods表的成员个数。使用两个字节来表示。methods表中每个成员都是一个method_info结构。

methods [](方法表)

  • methods表中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个
  • method_info结构的access_flags项既没有设置ACC_NATIVE标志也没有设置ACC_ABSTRACT标志,那么该结构中也应包含实现这个方法所用的Java虚拟机指令。
  • method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法·方法表的结构实际跟字段表是一样的,方法表结构如下:

image-20211219000018013

方法表访问标志
跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:

image-20211219000216789

8.7.8 属性表

属性表集合(attributes)

参考地址:jvm类文件解析官网

方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class 文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。

此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。

属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与己有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。

attributes [ ](属性表)
属性表的每个项的值必须是attribute_info结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。

属性的通用格式:

image-20211219161346983

即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义。

属性类型:
属性表实际上可以有很多类型,上面看到的Code属性只是其中一种,Java8里面定义了23种属性。下面这些是虚拟机中预定义的属性:

image-20211219161231071

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

image-20211219161303925

属性LineNumberTable

image-20211219160924020

属性LocalVariableTable

image-20211219154952811

The Attribute SourceFile

image-20211219161514373

8.2字节码指令

(字节码指令参考地址)

image-20211219164504682

使用javap返编译HelloWorld.java,得到如下内容

/*
源代码
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
*/

javap -v HelloWorld.class
Classfile /D:/IDE2019/code/thread/target/classes/com/compass/demo/HelloWorld.class
    
   //最后修改时间 和大小
  Last modified 2021-12-19; size 567 bytes
  // MD5 的校验签名    
  MD5 checksum 1e56216f3848c188487c07250204b356
  // java源文件    
  Compiled from "HelloWorld.java"
// 类的全路径名称      
public class com.compass.demo.HelloWorld
  minor version: 0
  // 版本号(jdk8)
  major version: 52
  // 访问修饰符    
  flags: ACC_PUBLIC, ACC_SUPER
// 常量池表      
Constant pool:
   // 方法引用 
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   // 引用成员变量    
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   // 引用23行的字符串常量  
   #3 = String             #23            // hello world
   // 方法引用    
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   // HelloWorld的全类名
   #5 = Class              #26            // com/compass/demo/HelloWorld
   // Object全类名    
   #6 = Class              #27            // java/lang/Object
   // 字符串常量
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/compass/demo/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/compass/demo/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  // 构造方法  
  public com.compass.demo.HelloWorld();
    // 参数类型和返回值类型描述
    descriptor: ()V
    // 方法修饰符    
    flags: ACC_PUBLIC
    // 代码结构体   
    Code:
      // 栈的最大深度(决定栈帧内存大小),局部变量表长度,参数个数 
      stack=1, locals=1, args_size=1
         // 将第一个局部变量推至栈顶 
         0: aload_0
         // 调用Object的init方法    
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         // 执行完毕返回
         4: return
      LineNumberTable:
        line 11: 0
      // 局部变量表      
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/compass/demo/HelloWorld;

  // main 方法
  public static void main(java.lang.String[]);
    // 参数类型和返回值类型描述
    descriptor: ([Ljava/lang/String;)V
    // 访问权限修饰符              
    flags: ACC_PUBLIC, ACC_STATIC
    // 代码结构体              
    Code:
      stack=2, locals=1, args_size=1
(字节码行号) 0: getstatic  #2               // Field java/lang/System.out:Ljava/io/PrintStream;
         // 加载常量池中的字符串         
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      // java源代码行号表            
      LineNumberTable:
        // java源码行号,字节码行号       
        line 13: 0
        line 14: 8
       // 局部变量表           
      LocalVariableTable:
      // 作用范围0~9   ,槽位0 ,名称,数据类型            
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

8.2.1 图解方法执行流程

原始代码:

public class HelloWorld {
    public static void main(String[] args) {
       int a=10;
       int b=Short.MAX_VALUE+1;
       int c=a+b;
        System.out.println(c);
    }
}

使用javap反编译后的字节码文件:

javap -v -p  HelloWorld.class
Classfile /D:/IDE2019/code/thread/target/classes/com/compass/demo/HelloWorld.class
  Last modified 2021-12-19; size 626 bytes
  MD5 checksum d6976925efb6239f87dc528578bd6224
  Compiled from "HelloWorld.java"
public class com.compass.demo.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()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/compass/demo/HelloWorld
   #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/compass/demo/HelloWorld;
  #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               HelloWorld.java
  #25 = NameAndType        #8:#9          // "<init>":()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/compass/demo/HelloWorld
  #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.compass.demo.HelloWorld();
    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 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/compass/demo/HelloWorld;

  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 13: 0
        line 14: 3
        line 15: 6
        line 16: 10
        line 17: 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: "HelloWorld.java"

1.常量池载入运行时常量池 ,在没有进入运行时常量池之前,这些字节码都是助记符,也就是符号引用

image-20211219180035563

方法字节码载入方法区

image-20211219180115684

main 线程开始运行,分配栈帧内存

image-20211219181857985

执行引擎开始执行字节码

  • bipush 10
    • 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
    • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
    • ldc 将一个 int 压入操作数栈
    • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
    • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池

image-20211219201819515

istore_1 将操作数栈顶数据弹出,存入局部变量表的 slot 1

image-20211219201858335

image-20211219201925027

ldc #3
从常量池加载 #3 数据到操作数栈
注意 : Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

image-20211219202116907

istore_2

image-20211219202814857

image-20211219202258107

iload_1

image-20211219202711469

iload_2

image-20211219202336000

iadd

image-20211219202435603

image-20211219203102568

istore_3

image-20211219203252241

image-20211219203308530

getstatic #4

image-20211219203546571

image-20211219204201929

iload_3

image-20211219204240976

image-20211219204311654

invokevirtual #5

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V
  • 方法 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码

image-20211219203956048

  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容

image-20211219204111478

8.2.2 练习:分析i++

public class HelloWorld {
    public static void main(String[] args) {
    int a=10;
    int b=a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}

对应的字节码:

javap -v -p  HelloWorld.class
Classfile /D:/IDE2019/code/thread/target/classes/com/compass/demo/HelloWorld.class
  Last modified 2021-12-19; size 601 bytes
  MD5 checksum b03579c863b1d7db78c2683ba2a163d8
  Compiled from "HelloWorld.java"
public class com.compass.demo.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #4 = Class              #27            // com/compass/demo/HelloWorld
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/compass/demo/HelloWorld;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               SourceFile
  #21 = Utf8               HelloWorld.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = Class              #29            // java/lang/System
  #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               com/compass/demo/HelloWorld
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  public com.compass.demo.HelloWorld();
    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 17: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/compass/demo/HelloWorld;

  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
         // 先push一个10到操作数栈         
         0: bipush        10
         // 将10从操作数栈弹出赋值给a  a=10         
         2: istore_1
         // 将a的值推至栈顶 10        
         3: iload_1
         // (对那个槽位自增,自增多少?)a=10+1 直接在局部变量表中增加,并没有影响的操作数栈
         4: iinc          1, 1
         // 对局部变量表中的 a=11+1 ,a=12  
         7: iinc          1, 1
         // 将局部变量表中的a=12再次推入栈顶         
        10: iload_1
        // 将栈中的两个int类型的数值相加并推入栈顶 10+12=22           
        11: iadd
        // 将局部变量表中1号槽位的a=12推向栈顶        
        12: iload_1
        //  将1号槽位的a-1(a=11)         
        13: iinc          1, -1
        // 将栈中的两个int类型的数值相加并推入栈顶 12+22=34         
        16: iadd
        // 获取操作数栈顶的元素放入到局部变量表的2号槽位 b=34          
        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 19: 0
        line 20: 3
        line 21: 18
        line 22: 25
        line 23: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
}

分析:

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算
  • ++a: 先执行iinc再执行load
  • a++:先执行load在iinc

8.2.3 条件判断指令

image-20211219215140692

说明: byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节 goto 用来进行跳转到指定行号的字节码

代码:

 public static void main(String[] args) {
        int a = 0;
        if(a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }

使用javap编译后的字节码

// 将int类型的0推入栈顶
0: iconst_0 
//  将栈顶int类型的0存入a变量   
1: istore_1
// 从局部变量表中把a加入到栈顶   
2: iload_1
// 当栈顶中的操作数不为0时跳转到12行 ,如果为0就不进行跳转继续向下执行  
3: ifne          12
// 往栈顶put一个10    
6: bipush        10
// 将栈顶的操作数复制给a=10    
8: istore_1
// 无条件跳转到15行    
9: goto          15
// 往栈顶put一个15    
12: bipush        20
// 将栈顶的操作数赋值给a    
14: istore_1
// 执行结束    
15: return
 LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     a   I
    
    

8.2.4 循环控制命令

java源代码

  public static void main(String[] args) {
        int a = 0;
        while (a < 10) {
            a++;
        }
    }

使用javap编译后的字节码

 Code:
      stack=2, locals=2, args_size=1
         // 将int类型的0推入栈顶 
         0: iconst_0
         // 将栈顶的操作数赋值给a    
         1: istore_1
         // 将a变量的值put到栈顶    
         2: iload_1
         // put一个10到操作数栈顶中    
         3: bipush        10
         //  将栈顶中的两个操作数进行比较是否大于等于,成立向下执行,否则大于等于时无条件跳转到14行   
         5: if_icmpge     14
         // 局部变量表中的第1一个槽位的变量自增1    
         8: iinc          1, 1
         // 无条件跳转回2行    
        11: goto          2
        // 执行结束    
        14: return
 LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            2      13     1     a   I

练习:

请从字节码角度分析,下列代码运行的结果:

    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x);
    }
 Code:
      stack=2, locals=3, args_size=1
         // 将int类型的0put到栈顶 
         0: iconst_0
         // 将栈顶的0赋值给i(i=0)    
         1: istore_1
          // 将int类型的0put到栈顶     
         2: iconst_0
          // 将栈顶的0赋值给x(x=0)        
         3: istore_2
         // 将iput到栈顶    
         4: iload_1
         // put一个10到栈顶    
         5: bipush        10
         // 比较栈顶两个操作数的大小, 0-10>=0时跳转到21,否则继续向下执行   
         7: if_icmpge     21
         // 将x(x=0) put到栈顶    
        10: iload_2
        // 将局部变量表中的x自增1    
        11: iinc          2, 1
        //  将栈顶的值赋值给x(x=0)   
        14: istore_2
        //  将局部变量表中的i自增1   
        15: iinc          1, 1
        // 无条件跳转到第4行    
        18: goto          4
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_2
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return
      LineNumberTable:
        line 16: 0
        line 17: 2
        line 18: 4
        line 19: 10
        line 20: 15
        line 22: 21
        line 23: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  args   [Ljava/lang/String;
            2      27     1     i   I
            4      25     2     x   I

8.2.5 构造方法

**cinit()V : ** 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V

源代码:

public class Demo {
    static int i = 10;

    public Demo() {
    }

    static {
        i = 20;
        i = 30;
    }
}   

字节码:

 stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return

init()V : 编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构 造方法内的代码总是在最后

java源代码:

public class Demo {
    private String a = "hello";
    {
        b = 20;
    }
    private int b = 10;
    {
        a = "world";
    }
    public Demo(String a, int b) {
        this.a = a;
        this.b = b;
    }
    public static void main(String[] args) {
        Demo d = new Demo("new", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
    
}

字节码:


stack=4, locals=2, args_size=1
         0: new           #6                  // class com/compass/Demo
         3: dup
         4: ldc           #7                  // String new
         6: bipush        30
         8: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        11: astore_1
        12: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: aload_1
        16: getfield      #3                  // Field a:Ljava/lang/String;
        19: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        25: aload_1
        26: getfield      #4                  // Field b:I
        29: invokevirtual #11                 // Method java/io/PrintStream.println:(I)V
        32: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
           12      21     1     d   Lcom/compass/Demo;

8.2.6 方法调用

java源代码:

public class Demo {
   private void test1(){

   }
    private final void test2(){

    }
    public static void test3(){

    }
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.test1();
        demo.test2();
        demo.test3();
    }
}

使用javap反编译后的字节码

invokespecial,invokestatic:静态绑定,在字节码生成的时候就能知道该调用那个类的方法,比 invokevirtual效率要高

invokevirtual: 动态绑定,只要到运行期间才能确定,即支持多态

静态方法要用类名去调用,不要使用对象,因为会多出两条字节码指令。

 Code:
      stack=2, locals=2, args_size=1
          // 创建一个Demo对象,并将引用值压入栈顶
         0: new           #2                  // class com/compass/Demo
         // 将栈顶的操作数赋值一份并且压入到栈顶    
         3: dup
         // 调用构造方法    
         4: invokespecial #3                  // Method "<init>":()V
         // 将栈顶中的操作数赋值给demo    
         7: astore_1
         // 将局部变量表中第二个元素压入到栈顶(demo)    
         8: aload_1
          // 调用test1()   
         9: invokespecial #4                  // Method test1:()V
         // 将局部变量表中第二个元素压入到栈顶(demo)        
        12: aload_1
         // 调用 test2()   
        13: invokespecial #5                  // Method test2:()V
         // 将局部变量表中第二个元素压入到栈顶(demo)   
        16: aload_1  // 多余的,因为静态方法根本不需要demo对象
         // 将栈顶的操作数弹出   
        17: pop    // 多余的,因为静态方法根本不需要demo对象
         // 调用test();   
        18: invokestatic  #6                  // Method test3:()V
         //将局部变量表中第二个元素压入到栈顶(demo)    
        21: aload_1
        // 调用test4()    
        22: invokevirtual #7                  // Method test4:()V
         // 结束   
        25: return
        LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  args   [Ljava/lang/String;
            8      18     1  demo   Lcom/compass/Demo;


8.2.7 多态原理

当执行invokevirtual指令时

  1. 先通过栈帧中的对象引用找到该对象
  2. 分析对象头找到对象的实际Class
  3. Class结构中有vtable,他在类加载的阶段已经根据方法的重写规则生成好了。
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

8.2.8 异常处理

1.try-catch

java源代码:

  public static void main(String[] args) throws IOException {

        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }

使用javap反编译后的字节码

 stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2      // 将异常对象放入局部变量表的2号槽位
         9: bipush        20
        11: istore_1
        12: return
      // 异常表      
      Exception table:
        // 监测 2~5行,如果出现Exception或Exception的子类异常就跳转到第8行
         from    to  target type
             2     5     8   Class java/lang/Exception
       // 局部变量表       
       LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/Exception;
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I


2.多个 single-catch 块的情况

java源代码

public static void main(String[] args)   {

        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 20;
        }catch (NullPointerException e){
            i=30;
        }catch (Exception e){
            i=40;
        }
    }

使用javap指令反编译后的字节码

 Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        20
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        30
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        40
        25: istore_1
        26: return
       // 异常表     
      Exception table:
         // 监测的都是2~5行,出现不同的异常跳转到不同的执行位置
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
    //  局部变量表         
	LocalVariableTable:
         // 槽位复用,因为始终只会出现一个异常,所以只用一个异常变量即可 
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
	

3.multi-catch 的情况

java源代码

public class Demo {
    public static void main(String[] args)   {
       try {
           Demo demo = new Demo();
           Method method = demo.getClass().getDeclaredMethod("method");
           method.invoke(demo,null);
       }catch (NoSuchMethodException |  InvocationTargetException | IllegalAccessException e){
           e.printStackTrace();
       }
    }

    public void method(){
        System.out.println("hello");
    }

}

使用javap反编译后的字节码

 Code:
      stack=3, locals=3, args_size=1
         0: new           #2                  // class com/compass/Demo
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        12: ldc           #5                  // String method
        14: iconst_0
        15: anewarray     #6                  // class java/lang/Class
        18: invokevirtual #7                 
       21: astore_2
        22: aload_2
        23: aload_1
        24: aconst_null
        25: invokevirtual #8                
        28: pop
        29: goto          37
        32: astore_1
        33: aload_1
        34: invokevirtual #12                 
        37: return
      Exception table:
         // 让三个不同的异常监测同一段代码,而且发生异常的入口也是同一个
         from    to  target type
             0    29    32   Class java/lang/NoSuchMethodException
             0    29    32   Class java/lang/reflect/InvocationTargetException
             0    29    32   Class java/lang/IllegalAccessException

4.finally

java源代码

 public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }

使用javap反编译后的字节码

 Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: bipush        30
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        20
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30
        24: istore_1
        25: aload_3
        26: athrow
        27: return
// 把finally语句块儿中的字节码复制一份,放一份在catch的后面,放一份在try的后面,放一份在catch没有匹配到合适异常的后面,
// 所以说无论如何finally语句块儿中的代码都会被执行到    
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any
     LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I


可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

finally练习题:

public static int test(){

    try {
    return 10;
    } finally {

     return 20;
    }
}

// 对应的字节码
Code:
      stack=1, locals=2, args_size=0
         0: bipush        10 // 将10put到栈顶
         2: istore_0         // 将栈顶的10弹出放入0号槽位
         3: bipush        20 // 将20put到栈顶
         5: ireturn          // 返回一个int类型的数值(20)
         6: astore_1         // 将一个any异常对象存入到1号槽位
         7: bipush        20 // 将20put到栈顶
         9: ireturn          // 返回一个int类型的数值(20)
      Exception table:
         from    to  target type
             0     3     6   any
// 最终无论如何都是返回的20             

如果在 finally 中出现了 return,会吞掉异常

// 捕捉不到异常,而且正常返回10
public static int test(){
    try {
         int i=1/0;
    } finally {
		return 10;
    }
 }
// 对应的字节码文件
stack=2, locals=2, args_size=0
         // 往栈顶中put一个int类型的1
         0: iconst_1
 	    // 往栈顶中put一个int类型的0
         1: iconst_0
         // 将操作数栈中两个数值相除,并且压入栈顶    
         2: idiv
         //  将栈顶中的int类型数组放入0号槽位   
         3: istore_0
         //  往栈顶中put一个10   
         4: bipush        10
          // 返回栈顶中的10   
         6: ireturn
          // 将栈顶中的int类型数组放入1号槽位     
         7: astore_1
         //  往栈顶中put一个10   
         8: bipush        10
         // 返回栈顶中的操作数 10    
        10: ireturn
      // 异常表      
      Exception table:
         from    to  target type
             0     4     7   any
// 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
             

finally对返回值的影响

public class Demo {
    public static void main(String[] args) {
        System.out.println(test());
    }

    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}
// 字节码指令
 Code:
      stack=1, locals=3, args_size=0
          // 将10put到栈顶
         0: bipush        10
          // 将10赋值给i   
         2: istore_0
         // 将 10推到栈顶    
         3: iload_0
         // 将10赋值给i,目的是为了固定返回值    
         4: istore_1
          // 将 20 推到栈顶        
         5: bipush        20
          // 将20put到0号槽位   
         7: istore_0
          // 将1号槽位的10put到栈顶   
         8: iload_1
         // 返回栈顶的int类型数值 10    
         9: ireturn
        10: astore_2
        11: bipush        20
        13: istore_0
        14: aload_2
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I

8.2.9 synchronized

   public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock){
            System.out.println("hello");
        }
    }
// 对应的字节码
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
          // 创建一个Object对象添加到栈顶        
         0: new           #2                  // class java/lang/Object
          // 将栈顶的Object对象复制一份放入栈顶,第一个是用于执行init()方法,第二个是用于加锁操作        
         3: dup
         // 调用Object对象的init方法         
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
          // 将栈顶的object对象赋值给1号槽位        
         7: astore_1
          // 把1号槽位的object对象引用put到栈顶         
         8: aload_1
         // 将栈顶的object对象复制一份,并且压入栈顶         
         9: dup
         // 将栈顶object对象对象放入到2号槽位         
        10: astore_2
         //  对lock引用指向的对象进行加锁操作        
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String hello
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        // 将2号槽位lock引用指向的对象加载到栈顶          
        20: aload_2
        // 对lock引用指向的对象进行解锁操作       
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
       //  对lock引用指向的对象进行解锁操作                  
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      // 异常表对这些代码都进行检测,如果出现异常都会跳转到25行,能够确保锁能被释放开            
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

8.3 编译期处理

8.3.1 默认构造器

//java源代码
public class Demo {

}
// 经过编译器编译后,默认加上一个无参的构造方法,并且去调用父类的构造方法(和字节码等价的伪代码)
public class Demo {
  public Demo(){
        super();
    }
}
  

8.3.2 自动拆装箱

这个特性是 JDK 5 开始加入的, 代码片段1 :

public class Demo {

    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}

上面这段代码在 JDK 5 之前是无法编译通过的,必须改写为 代码片段2 :

public class Demo {

    public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }

}

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是
包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即代码片段1 都会在编
译阶段被转换为 代码片段2

8.3.3 泛型集合取值

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

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 实际调用的是 List.add(Object e)
        Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
    }

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

// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);

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

// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

 Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
       // 虽然没有被擦除,但是不能通过反射机制获取到                              
       LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;
                                

只有泛型信息在方法参数,返回值上,才可以通过反射的机制获取到。

  public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
        return null;
    } 
public static void main(String[] args) throws NoSuchMethodException {

        Method test = Demo.class.getMethod("test", List.class, Map.class);
        Type[] types = test.getGenericParameterTypes();
        for (Type type : types) {
            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                System.out.println("原始类型 - " + parameterizedType.getRawType());
                Type[] arguments = parameterizedType.getActualTypeArguments();
                for (int i = 0; i < arguments.length; i++) {
                    System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
                }
            }
        }
    }

8.3.4 可变参数

可变参数也是 JDK 5 开始加入的新特性:

    public static void main(String[] args)   {

        test("a","b","c");
    }

    public static void test(String... args){
        System.out.println(Arrays.toString(args));
    }
 public static void test(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         // Method java/util/Arrays.toString:([Ljava/lang/Object;)Ljava/lang/String;         
         4: invokestatic  #8                  
         7: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 31: 0
        line 32: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
// 根据实参的个数创建一个参数对应类型的数组,如果传递的是null,会创建一个长度为0的数组                                     

8.3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖,数组的循环:

    public static void main(String[] args)   {
        int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
        for (int value : array) {
            System.out.println(value);
        }

    }

会被编译器转换为:

 public static void main(String[] args)   {
        int[] array = new int[]{1, 2, 3, 4, 5};
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }

    }

而集合的循环:

  public static void main(String[] args)   {
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        for (Integer i : list) {
            System.out.println(i);
        }

    }

实际被编译器转换为对迭代器的调用:

 public static void main(String[] args) {
        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);
        }
    }

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

8.3.6 switch 字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

    public static void main(String[] args) {
        choose("hello");
    }

    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }

注意 switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚

 public static void main(String[] args) {
      choose("hello");
    }

    public static void choose(String str) {
        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");
        }
    }

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可 能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的hashCode值都是 2123

8.3.7 switch 枚举

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

public class Demo {
    public static void main(String[] args) {
        foo(Sex.MALE);
    }

    public static void foo(Sex sex) {

        switch (sex) {
            case MALE:
                System.out.println("男");
                break;
            case FEMALE:
                System.out.println("女");
                break;
        }
    }

}


enum Sex {
    MALE, FEMALE;

}

转换后代码:

public class Demo {
    public static void main(String[] args) {
        foo(Sex.MALE);
    }
/**
* 定义一个合成类(仅 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;
        }
    }

}


enum Sex {
    MALE, FEMALE;
    
}

8.3.8 枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例:

enum Sex {
	MALE, FEMALE
}

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);
    }
}

8.2.9 try-with-resources

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

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

   try (FileInputStream in=new FileInputStream("a")){
            FileChannel channel = in.getChannel();
        }catch (Exception e){
            e.printStackTrace();
        }

会被转换为: 无论是我们字节代码块中的异常还是关闭资源时的异常都不会丢失

		try {
            InputStream is = new FileInputStream("d:");
            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();
        }
    }

8.2.10 0 方法重写时的桥接方法

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

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类
class Father{
    public List<String> get(){
        return new ArrayList<>();
    }
}

class Son extends Father{
    @Override
    public LinkedList<String> get() {
        return new LinkedList<>();
    }
}

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

class Father{
    public List<String> get(){
        return new ArrayList<>();
    }
}
class Son extends Father{
    @Override
    public  bridge LinkedList<String> get() {
        return new LinkedList<>();
    }
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public List get() 没有命名冲突,可以 用下面反射代码来验证:

  for (Method method : Son.class.getDeclaredMethods()) {
            System.out.println(method);
        }

输出结果:

  for (Method method : Son.class.getDeclaredMethods()) {
            System.out.println(method);
        }

8.2.11 匿名内部类

源代码:

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override

            public void run() {
                System.out.println("ok");
            }
        };
    }
}

转换后代码:

// 额外生成的类
final class Candy11$1 implements Runnable {
    Candy11$1() {
    }
    public void run() {
        System.out.println("ok");
    }
}

public class Candy11 {
    public static void main(String[] args) {
        Runnable runnable = new Candy11$1();
    }
}

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

public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
            }
        };
    }
}

转换后代码:

// 额外生成的类
final class Candy11$1 implements Runnable {
    int val$x;
    Candy11$1(int x) {
        this.val$x = x;
    }
    public void run() {
        System.out.println("ok:" + this.val$x);
    }
}

public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
}

注意: 这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11 1 对象的 v a l 1 对象的 val 1对象的valx 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化。

8.4 类加载阶段

image-20211221135307896

8.4.1 加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • java_mirror (即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用,相互含有对方的指针)
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意:nstanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror,是存储在堆中,可以通过前面介绍的 HSDB 工具查

image-20211221135734340

  1. 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。

  2. Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

  3. 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。

  4. 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口

8.4.2 链接

  • 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
  • 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
  • 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,
    • 为 static 变量分配空间,设置默认值
    • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
    • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析:虚拟机常量池的符号引用替换为字节引用过程

8.4.3 初始化

初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

导致类初始化的情况,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时
public class Loading {
    static {
        System.out.println("main init");
    }

    public static void main(String[] args) throws Exception {

        //  不会导致类初始化的情况
        // 1. main方法所在的类总是先别初始化
        // 2. 访问静态常量不会被初始化
        // System.out.println(B.b);
        // 3.访问类对象不会初始化
        // System.out.println(A.class);
        // 4.创建该类的数组对象不会导致该类初始化
        // A[] as = new A[10];
        // 5.classLoad会导致类的加载,但是不会初始化类
        // Thread.currentThread().getContextClassLoader().loadClass("com.compass.demo.A");
        // 6.Class.forName("全类名",false,classLoad)不会导致类的初始化

        //  会导致类初始化的情况
        // 1.首次访问某个类的静态变量或静态方法会导致这个类的初始化
        // A.show1();
        // 2.子类初始化,如果父类还没有初始化,会导致父类初始化(父类的初始化在子类之前)
        // B.show2();
        // 3.子类访问父类的静态变量,会导致父类初始化,并不会导致子类初始化
        // System.out.println(B.init);
        // 4.使用Class.fromName("全类名") 会导致该类被初始
        System.out.println(Class.forName("com.compass.demo.B"));

    }
}

class A {

    static int init=20;
    static {

        System.out.println("A init");
    }

    public static void show1(){

    }
}

class B extends A{
    final static int b = 10;
    static boolean c = true;

    static {
        System.out.println("B init");
    }
    public static void show2(){

    }
}

练习:判断访问类E中的变量会不会导致该类被初始化

public class Loading {

    public static void main(String[] args) throws Exception {
        System.out.println(E.a);
         System.out.println(E.b);
         System.out.println(E.c);
    }
}

class E {
    // 访问该变量不会导致类被初始化,因为基本类型的常量在类链接的阶段就准备好了
    public static final int a = 10;
    // 访问该变量不会导致类被初始化,因为基本类型的常量在类链接的阶段就准备好了
    public static final String b = "hello";
    // 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成(Integer是引用类型)
    public static final Integer c = 20;

    static {
        System.out.println("E init");
    }
}

典型案例:使用类加载机制完成单例模式(懒汉式)

public class Loading {

    public static void main(String[] args) throws Exception {
        System.out.println(Single.getInstance()==Single.getInstance());
    }
}

class Single {

    private Single(){

    }

    public static Single getInstance(){
        return Instance.SINGLE;
    }


// 访问外部类的时候,并不会导致Instance类被加载,只有Single访问Instance的属性才会导致Instance的初始化,而且是线程安全的
    private static final class Instance{
        private static final Single SINGLE = new Single();

    }
}

8.4.5 类加载器

以 JDK 8 为例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UH8Ju4ka-1666016850055)(C:%5CUsers%5C14823%5CDesktop%5Clearn-note%5Ctyproa-img%5Cimage-20211221151208358.png)]

image-20211221151810057

1 启动类加载器(bootstrap class loader)

它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2 扩展类加载器(extensions class loader)

扩展类加载器是指Sun公司实现的 sun.misc.Launcher$ExtClassLoader 类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

Xbootclasspath 表示设置 bootclasspath
其中 /a:. 表示将当前目录追加至 bootclasspath 之后
可以用这个办法替换核心类
- java -Xbootclasspath:<new bootclasspath>
- java -Xbootclasspath/a:<追加路径>
- java -Xbootclasspath/p:<追加路径>

3 系统类加载器(Application ClassLoader )

被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的 -classpath 选项、java.class.path系统属性,或者CLASSPATH将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。(Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式

比较两个类相等,只有这两个类是由同一个类加载器所加载的才有可比的意义,否则即使是同一个class文件,被同一个java’虚拟机加载,只要加载他们的类加载器不一致,那么这两个类必然不相等。

8.4.5.1 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
			// 1. 检查该类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
					// 2. 有上级的话,委派上级 loadClass
                        c = parent.loadClass(name, false);
                    } else {
						// 3. 如果没有上级了(ExtClassLoader),则委派
                        BootstrapClassLoader
                                c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) {
                    long t1 = System.nanoTime();
					// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                    c = findClass(name);
					// 5. 记录耗时
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }	
public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Hello.class.getClassLoader()
                .loadClass("com.compass.demo.Hello");
        System.out.println(aClass.getClassLoader());
    }

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher A p p C l a s s L o a d e r / / 2 处,委派上级 s u n . m i s c . L a u n c h e r AppClassLoader // 2 处,委派上级 sun.misc.Launcher AppClassLoader//2处,委派上级sun.misc.LauncherExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader
    查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 Hello这个类,显然没有
  6. sun.misc.Launcher E x t C l a s s L o a d e r / / 4 处,调用自己的 f i n d C l a s s 方法,是在 J A V A H O M E / j r e / l i b / e x t 下找 H e l l o 这个类,显然没有,回到 s u n . m i s c . L a u n c h e r ExtClassLoader // 4 处,调用自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 Hello这个类,显然没有,回到 sun.misc.Launcher ExtClassLoader//4处,调用自己的findClass方法,是在JAVAHOME/jre/lib/ext下找Hello这个类,显然没有,回到sun.misc.LauncherAppClassLoader
    的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在
    classpath 下查找,找到了

**双亲委派:**当一个类加载器收到加载类的任务时,会先让父类加载器去加载,父类加载器也会优先让本身的父类加载器去加载,依次类推,直至启动类加载器。

**好处:**这样做的好处就是避免类的重复加载,保护了核心的API库。

8.4.5.2 线程上下文加载器

线程上下文加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoader()这个方法进行设置,如果创建线程时还未设置,他可以从父线程中继承一个,如果在应用程序的全局范围内,都没有设置过的话,那个这个类加载器默认就是应用程序类加载器(AppClassLoader);

我们在使用 JDBC 时,都需要加载 Driver 驱动,在高本版的MySQL驱动下,不写Class.forName(“com.mysql.cj.jdbc.Driver”);也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader())

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

   private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>
                    () {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
// 1)使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers =
                        ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
// Do nothing
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载 再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI) 约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

image-20211221180825202

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

    public static <S> ServiceLoader<S> load(Class<S> service) {
		// 获取线程上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:

  private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                    "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                    "Provider " + cn + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error(); // This cannot happen
    }

8.4.5.3 自定义类加载器

什么时候需要自定义类加载器?

  • 想加载非 classpath 随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法 (注意:不要重写 loadClass 方法,否则不会走双亲委派机制)
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法
public class MyClassLoader extends ClassLoader {
    // 将来根据name去找到类文件
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "C:\\Users\\14823\\Desktop\\code\\"+name+".class";
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            // 将.class文件拷贝到一个字节数组中去
            Files.copy(Paths.get(path), os);
            // 得到字节数组
            byte[] bytes = os.toByteArray();

            // 调用父类的defineClass将bytes->.class
            return  defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("class文件未找到"+e.getMessage());
        }

    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException{
        System.out.println("name="+name);
        // 1、找到ext classLoader,并首先委派给它加载,为什么?
        ClassLoader classLoader = getSystemClassLoader();
        while (classLoader.getParent() != null) {
            classLoader = classLoader.getParent();
        }
        Class<?> clazz = null;
        try {
            clazz = classLoader.loadClass(name);
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        if (clazz != null) {
            return clazz;
        }
        // 2、自己加载
        clazz = this.findClass(name);
        if (clazz != null) {
            return clazz;
        }
        // 3、自己加载不了,再调用父类loadClass,保持双亲委派模式
        return super.loadClass(name);
    }

}

class Test{
    public static void main(String[] args) throws Exception {
        MyClassLoader loader1 = new MyClassLoader();
        MyClassLoader loader2 = new MyClassLoader();
        Class<?> c1 = loader1.loadClass("MapImplOne");
        Class<?> c2 = loader2.loadClass("MapImplOne");
        System.out.println(c1==c2);

    }
}
  • defineClass()方法的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象。
  • findClass()方法的主要职责就是找到.class文件并把.class文件读到内存得到字节码数组,然后调用 defineClass方法得到 Class 对象。子类必须实现findClass。
  • loadClass()方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。

8.6 运行期优化

8.6.1 即时编译

先来个例子: 循环200次,每次创建1000个对象

    public static void main(String[] args) {
        // 循环200次,每次创建1000个对象
        for (int i = 1; i <= 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("创建对象次数=%d\t耗时=%d\n",i,(end - start));
        }
    }
// 执行结果:到160多次的时候,耗时就不超过后400毫秒了。越来越快

JVM 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)
  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4 层,使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等

即时编译器(JIT)与解释器(Interpreter)的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),对热点代码进行优化。

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:- DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果。

8.6.2 方法内联

private static int square(final int i) {
		return i * i;
}

System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:

System.out.println(9 * 9)

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

System.out.println(81);
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印 inlining 信息
    // -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
    // -XX:+PrintCompilation 打印编译信息
    public static void main(String[] args) {
        int x = 0;
        for (int i = 0; i < 500; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
        }
    }
    private static int square(final int i) {
        return i * i;
    }

8.6.3 字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/

创建 maven 工程,添加依赖如下

<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {
    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    /**
     *  如果开启内联优化,三个方法最终都会被优化为test2()
     *  如果没有开启内联优化,那么jvm不会帮我们进行优化,test()的运行效率最高
     */
    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }

    static int sum = 0;
                              // INLINE 允许方法内联
    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    static void doSum(int x) {
        sum += x;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();
        new Runner(opt).run();
    }
}

8.6.4 反射优化

public class Loading {

    public static void foo() {
        System.out.println("foo...");
    }
    public static void main(String[] args) throws Exception {
        Method foo = Loading.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor NativeMethodAccessorImpl 实现

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }
    public Object invoke(Object target, Object[] args)
            throws IllegalArgumentException, InvocationTargetException {
// inflationThreshold 膨胀阈值,默认 15
        if (++this.numInvocations > ReflectionFactory.inflationThreshold()
                && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass()))
        {
// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl generatedMethodAccessor =
                    (MethodAccessorImpl)
                            (new MethodAccessorGenerator())
                                    .generateMethod(
                                            this.method.getDeclaringClass(),
                                            this.method.getName(),
                                            this.method.getParameterTypes(),
                                            this.method.getReturnType(),
                                            this.method.getExceptionTypes(),
                                            this.method.getModifiers()
                                    );
            this.parent.setDelegate(generatedMethodAccessor);
        }
// 调用本地实现
        return invoke0(this.method, target, args);
    }
    void setParent(DelegatingMethodAccessorImpl parent) {
        this.parent = parent;
    }
    private static native Object invoke0(Method method, Object target, Object[]
            args);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1

注意 通过查看 ReflectionFactory 源码可知 sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首 次生成比较耗时,如果仅反射调用一次,不划算) sun.reflect.inflationThreshold 可以修改膨胀阈值。

9. JVM 性能检测工具

命令行方式:

1.jps:查看正在运行的java运行进程 常用: jsp -l :查看正在运行的java进程,顺便显示全类名

2.jstat:用于监视虚拟机各种运行信息,本地虚拟机类装载,内存,垃圾回收,JIT编译等运行数据

//1. 查看类的加载信息
jstat -class 13084(java进程id)
// 加载类个数,加载类字节数,卸载的类,卸载了字节数,时间    
   Loaded  Bytes  Unloaded  Bytes     Time
    610  1233.0        0     0.0       0.11
    
// 2.interval参数,用于输出统计数据的周期,单位为毫秒,查询间隔
jstat -class 13084(java进程id) 10000(间隔时间)
    
// 3.查看程序运行时间
jstat -class -t 13084(java进程id)    
 
// 4.每个多长时间打印一下表头信息
 jstat -class -t -h3 13084(java进程id) 1000(时间间隔,毫秒)   
     
// 5.查看编译后的类总数
jstat -compiler 13084(java进程id)     
     
// 6.查看编译后的方法信息
jstat -printcompilation 13084(java进程id)     

// 7.显示与GC相关的信息
jstat -gc 13084(java进程id)
  
jstat -gcutil 13084   
 
jstat -gccause 13084    
     
    
    
    

3.jinfo:查看虚拟机参数配置信息,也可以调整虚拟机的配置参数

// 1.查看java虚拟机参数信息
jinfo -flags  13084(java进程id)
// 2.可以获取System.getProperties()取得的参数
jinfo -sysprops 13084(java进程id)
// 3.查看具体某个jvm参数
jinfo -flag MaxNewSize(虚拟机参数)  13084(java进程id)   
// 4.筛选出那些可以动态修改的虚拟机参数(windows)
java -XX:+PrintFlagsFinal -version | findstr "manageable" 
// 5.动态修改jvm参数(部分可修改)
 jinfo -flag +PrintGCDetails(需要修改的参数) 13084(java进程id)   
// 扩展
// 查看jvm所有参数的初始值:java -XX:+PrintFlagsInitial 
// 查看jvm所有参数的最终值:java -XX:+PrintFlagsFinal
// 查看jvm所有参数的最终值:java -XX:+PrintCommandLineFlags
    
    

4.jamp:查看java堆内存使用情况,到处内存情况文件

// 1.查看虚拟机内存使用情况
jmap -heap 21108(java进程id)
// 2.导出jvm内存使用情况(全部)
 jmap -dump:format=b,file=d:one.hprof 7484 (java进程id)  [需要特定的分析工具打开进行分析] 
//3.导出jvm内存使用情况(存活的对象)
jmap -dump:live,format=b,file=d:two.hprof 7484 (java进程id)  [需要特定的分析工具打开进行分析]     
// 4.程序出现oom的时候,dump出当时的内存快照
 -XX:HeapDumpOnOutMemoryError  -XX:HeapDumpPath=d:\heap.hprof (指定保存快照文件的路径)
// 5.查看当前时刻内存信息
jmap -histo 7484(java进程id)     
// jmap为了不被用户线程所干扰,需要借助安全点机制,让线程安全停下来,不改变堆中的数据,这可能导致分析结果有偏差  ,jstat不同,垃圾回收器会主动的把数据保存到固定的位置,而jstat只需要获取即可。    
     

5.jhat :jdk自内存快照分析工具

// 启动本地服务器
jhat one.hprof(快照文件) // 访问地址:http://localhost:7000/histo/
    

image-20211222181944101

6.jstack:用于生成当前线程

// 1.打印当前线程快照
jstack 7484(java进程id)

image-20211222182322736

image-20211222182620673

7.jcmd:可以实现前面的jstat之外的功能,还可以用它来导出堆,内存使用,查看java进程,导出线程信息,执行GC,JVM运行时间等待

// 1. 列出所有的jvm进程
jcm -l
// 2.列出jcm使用方法    
jcmd pid -help
// 3.显示指定进程的指令
jcmd pid 具体命令    

图形化界面工具:

1.jconsole: 查看内存,线程,执行GC,jvm参数等信息 cpu使用率 [jdk自带]

2.jvisualvm: [jdk自带]

3.jmc: [jdk自带]

4.MAT:MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。[第三方工具]

5.·Arthas:Alibaba开源的Java诊断工具。深受开发者喜爱。[第三方工具]

6.Btrace:Java运行时追踪工具。可以在不停机的情况下,跟踪指定的方法调用、杉造图线调用和系统内存等信息。[第三方工具]

附录:

1.jvm参数

#常用的设置
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。 

-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。 

-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。 

-XX:NewSize=n 设置年轻代初始化大小大小 

-XX:MaxNewSize=n 设置年轻代最大值

-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 13,年轻代占整个年轻代+年老代和的 1/4 

-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8

-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。

-XX:ThreadStackSize=n 线程堆栈大小

-XX:PermSize=n 设置持久代初始值	

-XX:MaxPermSize=n 设置持久代大小
 
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

#下面是一些不常用的

-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小

-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能

-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用	

-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动

-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用	

-Xnoclassgc 是否禁用垃圾回收

-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用	



Jvm GC 收集器设置

-XX:+UseSerialGC:设置串行收集器,年轻带收集器 

 -XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。

-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量

-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 

-XX:+UseConcMarkSweepGC:设置年老代并发收集器

-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器

1.参考文档


1、虚拟机参数设置文档
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

2、垃圾回收相关文档
http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html

3.jvm描述文档class
 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

参考连接:
https://blog.csdn.net/sunjin9418/article/details/79603651?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163972766916780255219875%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163972766916780255219875&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-79603651.first_rank_v2_pc_rank_v29&utm_term=java%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8&spm=1018.2226.3001.4187

https://blog.csdn.net/weixin_43122090/article/details/105093777?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163946865416780264088195%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163946865416780264088195&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-105093777.first_rank_v2_pc_rank_v29&utm_term=jvm&spm=1018.2226.3001.4187


线上分析: https://gceasy.io/

2.jvm助记符说明

指令码 助记符    说明
0x00 nop        无操作
0x01 aconst_null 将null推送至栈顶
0x02 iconst_m1    将int型-1推送至栈顶
0x03 iconst_0    将int型0推送至栈顶
0x04 iconst_1    将int型1推送至栈顶
0x05 iconst_2    将int型2推送至栈顶
0x06 iconst_3    将int型3推送至栈顶
0x07 iconst_4    将int型4推送至栈顶
0x08 iconst_5    将int型5推送至栈顶
0x09 lconst_0    将long型0推送至栈顶
0x0a lconst_1    将long型1推送至栈顶
0x0b fconst_0    将float型0推送至栈顶
0x0c fconst_1    将float型1推送至栈顶
0x0d fconst_2    将float型2推送至栈顶
0x0e dconst_0    将double型0推送至栈顶
0x0f dconst_1    将double型1推送至栈顶
0x10 bipush    将单字节的常量值(-128~127)推送至栈顶
0x11 sipush    将一个短整型常量值(-32768~32767)推送至栈顶
0x12 ldc    将int, float或String型常量值从常量池中推送至栈顶
0x13 ldc_w    将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14 ldc2_w    将long或double型常量值从常量池中推送至栈顶(宽索引)
0x15 iload    将指定的int型本地变量推送至栈顶
0x16 lload    将指定的long型本地变量推送至栈顶
0x17 fload    将指定的float型本地变量推送至栈顶
0x18 dload    将指定的double型本地变量推送至栈顶
0x19 aload    将指定的引用类型本地变量推送至栈顶
0x1a iload_0    将第一个int型本地变量推送至栈顶
0x1b iload_1    将第二个int型本地变量推送至栈顶
0x1c iload_2    将第三个int型本地变量推送至栈顶
0x1d iload_3    将第四个int型本地变量推送至栈顶
0x1e lload_0    将第一个long型本地变量推送至栈顶
0x1f lload_1    将第二个long型本地变量推送至栈顶
0x20 lload_2    将第三个long型本地变量推送至栈顶
0x21 lload_3    将第四个long型本地变量推送至栈顶
0x22 fload_0    将第一个float型本地变量推送至栈顶
0x23 fload_1    将第二个float型本地变量推送至栈顶
0x24 fload_2    将第三个float型本地变量推送至栈顶
0x25 fload_3    将第四个float型本地变量推送至栈顶
0x26 dload_0    将第一个double型本地变量推送至栈顶
0x27 dload_1    将第二个double型本地变量推送至栈顶
0x28 dload_2    将第三个double型本地变量推送至栈顶
0x29 dload_3    将第四个double型本地变量推送至栈顶
0x2a aload_0    将第一个引用类型本地变量推送至栈顶
0x2b aload_1    将第二个引用类型本地变量推送至栈顶
0x2c aload_2    将第三个引用类型本地变量推送至栈顶
0x2d aload_3    将第四个引用类型本地变量推送至栈顶
0x2e iaload    将int型数组指定索引的值推送至栈顶
0x2f laload    将long型数组指定索引的值推送至栈顶
0x30 faload    将float型数组指定索引的值推送至栈顶
0x31 daload    将double型数组指定索引的值推送至栈顶
0x32 aaload    将引用型数组指定索引的值推送至栈顶
0x33 baload    将boolean或byte型数组指定索引的值推送至栈顶
0x34 caload    将char型数组指定索引的值推送至栈顶
0x35 saload    将short型数组指定索引的值推送至栈顶
0x36 istore    将栈顶int型数值存入指定本地变量
0x37 lstore    将栈顶long型数值存入指定本地变量
0x38 fstore    将栈顶float型数值存入指定本地变量
0x39 dstore    将栈顶double型数值存入指定本地变量
0x3a astore    将栈顶引用型数值存入指定本地变量
0x3b istore_0    将栈顶int型数值存入第一个本地变量
0x3c istore_1    将栈顶int型数值存入第二个本地变量
0x3d istore_2    将栈顶int型数值存入第三个本地变量
0x3e istore_3    将栈顶int型数值存入第四个本地变量
0x3f lstore_0    将栈顶long型数值存入第一个本地变量
0x40 lstore_1    将栈顶long型数值存入第二个本地变量
0x41 lstore_2    将栈顶long型数值存入第三个本地变量
0x42 lstore_3    将栈顶long型数值存入第四个本地变量
0x43 fstore_0    将栈顶float型数值存入第一个本地变量
0x44 fstore_1    将栈顶float型数值存入第二个本地变量
0x45 fstore_2    将栈顶float型数值存入第三个本地变量
0x46 fstore_3    将栈顶float型数值存入第四个本地变量
0x47 dstore_0    将栈顶double型数值存入第一个本地变量
0x48 dstore_1    将栈顶double型数值存入第二个本地变量
0x49 dstore_2    将栈顶double型数值存入第三个本地变量
0x4a dstore_3    将栈顶double型数值存入第四个本地变量
0x4b astore_0    将栈顶引用型数值存入第一个本地变量
0x4c astore_1    将栈顶引用型数值存入第二个本地变量
0x4d astore_2    将栈顶引用型数值存入第三个本地变量
0x4e astore_3    将栈顶引用型数值存入第四个本地变量
0x4f iastore    将栈顶int型数值存入指定数组的指定索引位置
0x50 lastore    将栈顶long型数值存入指定数组的指定索引位置
0x51 fastore    将栈顶float型数值存入指定数组的指定索引位置
0x52 dastore    将栈顶double型数值存入指定数组的指定索引位置
0x53 aastore    将栈顶引用型数值存入指定数组的指定索引位置
0x54 bastore    将栈顶boolean或byte型数值存入指定数组的指定索引位置
0x55 castore    将栈顶char型数值存入指定数组的指定索引位置
0x56 sastore    将栈顶short型数值存入指定数组的指定索引位置
0x57 pop     将栈顶数值弹出 (数值不能是long或double类型的)
0x58 pop2    将栈顶的一个(long或double类型的)或两个数值弹出(其它)
0x59 dup     复制栈顶数值并将复制值压入栈顶
0x5a dup_x1    复制栈顶数值并将两个复制值压入栈顶
0x5b dup_x2    复制栈顶数值并将三个(或两个)复制值压入栈顶
0x5c dup2    复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
0x5d dup2_x1    复制栈顶的一个或两个值,将其插入栈顶那两个或三个值的下面
0x5e dup2_x2    复制栈顶的一个或两个值,将其插入栈顶那两个、三个或四个值的下面
0x5f swap    将栈最顶端的两个数值互换(数值不能是long或double类型的)
0x60 iadd    将栈顶两int型数值相加并将结果压入栈顶
0x61 ladd    将栈顶两long型数值相加并将结果压入栈顶
0x62 fadd    将栈顶两float型数值相加并将结果压入栈顶
0x63 dadd    将栈顶两double型数值相加并将结果压入栈顶
0x64 isub    将栈顶两int型数值相减并将结果压入栈顶
0x65 lsub    将栈顶两long型数值相减并将结果压入栈顶
0x66 fsub    将栈顶两float型数值相减并将结果压入栈顶
0x67 dsub    将栈顶两double型数值相减并将结果压入栈顶
0x68 imul    将栈顶两int型数值相乘并将结果压入栈顶
0x69 lmul    将栈顶两long型数值相乘并将结果压入栈顶
0x6a fmul    将栈顶两float型数值相乘并将结果压入栈顶
0x6b dmul    将栈顶两double型数值相乘并将结果压入栈顶
0x6c idiv    将栈顶两int型数值相除并将结果压入栈顶
0x6d ldiv    将栈顶两long型数值相除并将结果压入栈顶
0x6e fdiv    将栈顶两float型数值相除并将结果压入栈顶
0x6f ddiv    将栈顶两double型数值相除并将结果压入栈顶
0x70 irem    将栈顶两int型数值作取模运算并将结果压入栈顶
0x71 lrem    将栈顶两long型数值作取模运算并将结果压入栈顶
0x72 frem    将栈顶两float型数值作取模运算并将结果压入栈顶
0x73 drem    将栈顶两double型数值作取模运算并将结果压入栈顶
0x74 ineg    将栈顶int型数值取负并将结果压入栈顶
0x75 lneg    将栈顶long型数值取负并将结果压入栈顶
0x76 fneg    将栈顶float型数值取负并将结果压入栈顶
0x77 dneg    将栈顶double型数值取负并将结果压入栈顶
0x78 ishl    将int型数值左移位指定位数并将结果压入栈顶
0x79 lshl    将long型数值左移位指定位数并将结果压入栈顶
0x7a ishr    将int型数值右(符号)移位指定位数并将结果压入栈顶
0x7b lshr    将long型数值右(符号)移位指定位数并将结果压入栈顶
0x7c iushr    将int型数值右(无符号)移位指定位数并将结果压入栈顶
0x7d lushr    将long型数值右(无符号)移位指定位数并将结果压入栈顶
0x7e iand    将栈顶两int型数值作“按位与”并将结果压入栈顶
0x7f land    将栈顶两long型数值作“按位与”并将结果压入栈顶
0x80 ior     将栈顶两int型数值作“按位或”并将结果压入栈顶
0x81 lor     将栈顶两long型数值作“按位或”并将结果压入栈顶
0x82 ixor    将栈顶两int型数值作“按位异或”并将结果压入栈顶
0x83 lxor    将栈顶两long型数值作“按位异或”并将结果压入栈顶
0x84 iinc    将指定int型变量增加指定值(i++, i--, i+=2)
0x85 i2l     将栈顶int型数值强制转换成long型数值并将结果压入栈顶
0x86 i2f     将栈顶int型数值强制转换成float型数值并将结果压入栈顶
0x87 i2d     将栈顶int型数值强制转换成double型数值并将结果压入栈顶
0x88 l2i     将栈顶long型数值强制转换成int型数值并将结果压入栈顶
0x89 l2f     将栈顶long型数值强制转换成float型数值并将结果压入栈顶
0x8a l2d     将栈顶long型数值强制转换成double型数值并将结果压入栈顶
0x8b f2i     将栈顶float型数值强制转换成int型数值并将结果压入栈顶
0x8c f2l     将栈顶float型数值强制转换成long型数值并将结果压入栈顶
0x8d f2d     将栈顶float型数值强制转换成double型数值并将结果压入栈顶
0x8e d2i     将栈顶double型数值强制转换成int型数值并将结果压入栈顶
0x8f d2l     将栈顶double型数值强制转换成long型数值并将结果压入栈顶
0x90 d2f     将栈顶double型数值强制转换成float型数值并将结果压入栈顶
0x91 i2b     将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
0x92 i2c     将栈顶int型数值强制转换成char型数值并将结果压入栈顶
0x93 i2s     将栈顶int型数值强制转换成short型数值并将结果压入栈顶
0x94 lcmp    比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶
0x95 fcmpl    比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x96 fcmpg    比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x97 dcmpl    比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x98 dcmpg    比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x99 ifeq    当栈顶int型数值等于0时跳转
0x9a ifne    当栈顶int型数值不等于0时跳转
0x9b iflt    当栈顶int型数值小于0时跳转
0x9c ifge    当栈顶int型数值大于等于0时跳转
0x9d ifgt    当栈顶int型数值大于0时跳转
0x9e ifle    当栈顶int型数值小于等于0时跳转
0x9f if_icmpeq    比较栈顶两int型数值大小,当结果等于0时跳转
0xa0 if_icmpne    比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1 if_icmplt    比较栈顶两int型数值大小,当结果小于0时跳转
0xa2 if_icmpge    比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3 if_icmpgt    比较栈顶两int型数值大小,当结果大于0时跳转
0xa4 if_icmple    比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5 if_acmpeq    比较栈顶两引用型数值,当结果相等时跳转
0xa6 if_acmpne    比较栈顶两引用型数值,当结果不相等时跳转
0xa7 goto    无条件跳转到指定位置
0xa8 jsr     跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
0xa9 ret     返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用)
0xaa tableswitch    用于switch条件跳转,case值连续(可变长度指令)
0xab lookupswitch    用于switch条件跳转,case值不连续(可变长度指令)
0xac ireturn    从当前方法返回int
0xad lreturn    从当前方法返回long
0xae freturn    从当前方法返回float
0xaf dreturn    从当前方法返回double
0xb0 areturn    从当前方法返回对象引用
0xb1 return    从当前方法返回void
0xb2 getstatic    获取指定类的静态域,并将其值压入栈顶
0xb3 putstatic    为指定的类的静态域赋值
0xb4 getfield    获取指定类的实例域,并将其值压入栈顶
0xb5 putfield    为指定的类的实例域赋值
0xb6 invokevirtual    调用实例方法
0xb7 invokespecial    调用超类构造方法,实例初始化方法,私有方法
0xb8 invokestatic    调用静态方法
0xb9 invokeinterface 调用接口方法
0xba invokedynamic  调用动态链接方法
0xbb new     创建一个对象,并将其引用值压入栈顶
0xbc newarray    创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
0xbd anewarray    创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
0xbe arraylength 获得数组的长度值并压入栈顶
0xbf athrow    将栈顶的异常抛出
0xc0 checkcast    检验类型转换,检验未通过将抛出ClassCastException
0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
0xc2 monitorenter    获得对象的锁,用于同步方法或同步块
0xc3 monitorexit    释放对象的锁,用于同步方法或同步块
0xc4 wide    扩大本地变量索引的宽度
0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶
0xc6 ifnull    为null时跳转
0xc7 ifnonnull    不为null时跳转
0xc8 goto_w    无条件跳转
0xc9 jsr_w    跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶
=================最后三个为保留指令===========================
0xca breakpoint  调试时的断点标记
0xfe impdep1    为特定软件而预留的语言后门
0xff impdep2    为特定硬件而预留的语言后门

相关文章:

  • 用C++实现单例模式
  • Swift中的单例
  • 【个人博客搭建】(13)SqlSugar仓储实现
  • Rust的impl
  • C# 异步编程
  • 训练深度神经网络,使用反向传播算法,产生梯度消失和梯度爆炸问题的原因?
  • 论文阅读-CheckFreq:频繁、精细的DNN检查点操作。
  • 【Python从入门到进阶】49、当当网Scrapy项目实战(二)
  • Spring中的事务和事务的传播机制
  • uniapp android 原生插件开发-测试流程
  • vue使用gitshot生成gif
  • 小程序里.vue界面中传值的两种方式
  • AI(七)基础
  • CANalyst—Ⅱ 连通与手动收发测试、python收发测试
  • 类和对象基础(C++)
  • Maven简介、安装、使用、依赖传递
  • 11.MongoDB系列之连接副本集
  • 电子与电路复习题重点大题(附答案)
  • 【精品】seata综合示例:订单-库存-扣款
  • Spring常用注解的详细介绍(包你学明白)
  • Torchtext快速入门(一)——Vocab
  • 34461A数字万用表参数
  • AI加速(四)| 衣柜般的分层存储设计
  • Linux格式化输出当前时间
  • c++类和对象中
  • ATT汇编总结_9_静态库与动态库
  • VS Code For Web 深入浅出 -- 导读篇
  • 设计模式(一)前言
  • MyBatis
  • CAD机械零件平面绘制练习六
  • 相比Vue和React,Svelte可能更适合你
  • HTTP/HTTPS/TCP原理