JVM 中篇

字节码与类的加载篇

概述

java规范和虚拟机规范的关系

  • JAVA 规范指的是

    java语言的规范,使用符合java规范的编译器讲java编译成class。比如javac编译器。

  • JVM 规范指的是java虚拟机规范

    有很多实现,比如阿里的实现、华为的和ibm的实现。它们负责从导入class文件开始工作。

字节码文件的跨平台性

通过java编译器编译完成的文件class,可以在不同的平台上运行,只要平台上安装了JVM。

java的前端编译器

java编译器要完成编译有以下几个步骤。

  • 词法解析
  • 语法解析
  • 语义解析
  • 生成字节码

前段编译器和后端编译器的关系

image-20220510004545804

前端编译器编译后的文件有可能进入解释器被执行,也有可能成为热点代码进入JIT执行。在JIT中代码执行效率会高很多。这也是为什么java被定义为半解释半编译行语言。

其他前端编译器
  • javac
  • ECJ eclipse的
  • AspectJ IDEA的
后端编译器
  • AOT

    程序运行之前直接把文件翻译成机器指令。

    会破坏java的跨平台性。

    目前只处于linux中

  • JIT

    在HotSpot中将热点代码编译为机器码。

java文件看不到的执行过程

例1:Integer

1
2
3
4
5
6
7
        Integer i1 = 10;
Integer i2 = 10;
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
// 答案是 true false

再字节码中回去调用Integer.valueOf();

Integer的静态代码块中有堆一个定义的静态数组 cache进行赋值

从-128到127,并且再调用Integer.valueOf时间会比较是否在诉数组范围内,如果在的话就直接从cache[]取得。所以小于128的值的对比是true,应为是同一个对象。

image-20220508232312895

列2: String 的hello world拼接

image-20220510015029282

image-20220510015003627

image-20220510230629580

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0 new一个String builder
3 dup 不明
4 将调用init方法
7 new String
10 dup 不明
11 ldc 引入hello字面量
13 执行String的init方法
16 将String append到String builder
19 新建一个String
22 不明
23 引入字面两world
25 执行string的init
28 将string append到String Builder
31 toString 活的一个新字符串
34 放进操作数栈里
35 ldc #10 加载一个字面量 heloworld
37 放入操作数栈
38 getstatic 执行今天方法
aload_1 加载第一个字符串
aload_2 加载第二个字符串
if_acmpne 比较两个字符串

列3: 父子类

  • Father

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package org.akachi.classFile;

    /**
    * @Author akachi
    * @Email zsts@hotmail.com
    * @Date 2022/5/11 19:34
    */
    public class Father {
    public int x = 10;
    public Father(){
    print();
    x = 20;
    }
    public void print(){
    System.out.println("Father:"+x);
    }
    }

  • Son

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package org.akachi.classFile;

    /**
    * @Author akachi
    * @Email zsts@hotmail.com
    * @Date 2022/5/11 19:34
    */
    public class Son extends Father {
    int x = 30;
    public Son(){
    this.print();
    x = 40;
    }

    @Override
    public void print() {
    System.out.println("son x:"+this.x);
    }

    public static void main(String[] args) {
    new Son();
    }
    }

执行结果:

1
2
3
4
son x:0
son x:30

Process finished with exit code 0

执行 原理:

Father init执行过程

image-20220511222149779

Son的init方法

image-20220511222354594

成员变量的复制过程:

在调用Son的init方法时会去调用father的init方法,并且会调用father的init方法,但是调用的print方法确是子类重写的。但子类的x变了并未被复制所以这里打印出来的是子类的x的默认值0.

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Son extends Father {
int x = 30;
public Son(){
this.printSon();
x = 40;
}

public void printSon() {
System.out.println("son x:"+this.x);
}

public static void main(String[] args) {
new Son();
}
}

将代码改成以下使调用father时不会调用printSon就可以使值打赢正常。

变了默认初始化过程:

默认初始化、显示初始化、构造器初始化、有了对象后可以对象.属性或对象.方法的方式堆成员变量赋值。

Class文件结构

任何一个Class都对应一个Java类,但是并非所有class都是以磁盘的方式呈现的。

Class的本质是个二进制流,可以只存在于内存。

CLass文件采用类似于C语言结构体的方式来存储,这种结构中只有两种数据类型:无符号数和表。

  • 无符号数、以u1 u2 u4 u8 代表字节数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由一系列无符号数活其他作为数据项构成的符合数据类型,所以表都习惯性的以”_info”结尾。用于描述层次和关系的复合结构数据。整个Class文件本质上使一张表。由于表没有固定长度,所有通常会在其前面加上个数说明。

CLass组成部分:

  • 魔数
  • CLass文件版本
  • 常量池
  • 访问标志
  • 类索引、父类索引、接口索引
  • 字段表集合
  • 放发表集合
  • 属性表集合

解释:

The ClassFile Structure

A class file consists of a single ClassFile structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

The items in the ClassFile structure are as follows:

  • 魔数

    4个字节,标识编译器。识别它是一个class文件的标识。

  • 文件版本

    两个 U2 的版本表示大版本和小版本。

    minor_version、major_version;

  • 常量池

    • 2个字节的常量池长度标识constant_pool_count;
    • cp_info 长度是constant_pool[constant_pool_count-1]
  • 访问标识

    2个字节的access_flag

    表示类的类型、是类、接口、enum、有没有final?

  • 类索引、父类索引、接口结合索引

    • 2字节的类名 this_class;
    • 2字节的父类 super_class;
    • 2字节的接口数量 interfaces_count;
    • 2字节接口名 [interfaces_count]个;
  • 字段信息

    • 2字节 字段数量 fields_count
    • field_info fields_count个u2
  • 方法信息

    • 2字节的方法数量 methods_count
    • method_info 方法信息 [methods_count]个方法信息;
  • 属性表集合

    • 2个字节的属性表信息数量 attributes_count;
    • 属性信息 attribute_info attributes_count个属性信息;

什么是字节码

java源代码经过前端编译后形成的二进制文件,但是这是用于JVM执行的字节码指令,并非机器码。

JVM在执行字节码时一开始性能会差一些预热之后通过JIT将热点代码缓存后性能会达到与C相同的性能。

概念:

  • 操作码(opcode)

    代表特定含义的操作码

  • 操作数(operand)

    0个或多个

  • 虚拟机指令

    虚拟机指令有一个字节长度的代表特定含义的操作码和随后的0个至多个代表操作所需参数的操作数。

查看字节码文件方式:

  • 通过Javap

    1
    javap -v className.class
  • 通过Idea JClassLib 插件

  • 通过Binary Viewer 等工具查看。

参考资料 JAVA8虚拟机规范第四章:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

解读字节码

必要工具:

Binary Viewer

idea JClasslib:

字面量和符号引用:
标致 类型
F Float
I int
J long
S short
Z boolean
V void
L LJava/lang/object;
[ 数组类型,代表一维数组。比如:Str [] [] [] is [[[Ljava.lang.String;

在加载class文件实会建立动态链接库,将字常量池中的符号引用指向动态链接库 (Current Class Constant Pool Reference).

image-20220514100807446

建立动态链接库会在类的加载器子系统> 连接阶段的>解析阶段建立。

符号引用和直接引用的区别:
  • 符号引用

    相当于引用的mata,有一组符号表示如何建立应用。在class文件中存在。

  • 直接引用

    内存指针

常量表类型

image-20220514104434905

字节码指令集与解析举例

访问标识符

类索引

父类索引

接口索引集合

  • 计数器

会有一个标识符标识其接口的数量

字段

  • 计数器

  • image-20220708144633555

    字段名是在常量池中被声明的,字段名索引会指向常量池。

方法集合

类的加载过程详解

再谈类的加载器

参考

java 虚拟机规范

https://docs.oracle.com/javase/specs/index.html