一、字节码指令
反编译
- 可使用
javap -v className.class
来反编译class文件public class Demo3_1 { public static void main(String[] args) { int a = 10; int b = Short.MAX_VALUE + 1; int c = a + b; System.out.println(c); } }
Classfile /D:/downloads/资料 解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class Last modified 2020-11-2; size 635 bytes MD5 checksum 1a6413a652bcc5023f130b392deb76a1 Compiled from "Demo3_1.java" public class cn.itcast.jvm.t3.bytecode.Demo3_1 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 // cn/itcast/jvm/t3/bytecode/Demo3_1 #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 Lcn/itcast/jvm/t3/bytecode/Demo3_1; #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 Demo3_1.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 cn/itcast/jvm/t3/bytecode/Demo3_1 #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 cn.itcast.jvm.t3.bytecode.Demo3_1(); 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 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t3/bytecode/Demo3_1; 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 8: 0 line 9: 3 line 10: 6 line 11: 10 line 12: 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: "Demo3_1.java"
执行流程
- main方法中指明了局部变量表数目、操作数栈的深度、方法参数的数目
stack=2, locals=4, args_size=1
bipush 10
:将一个 byte 压入操作数栈istore_1
:将操作数栈顶数据弹出,存入局部变量表的slot 1ldc #3
:从常量池加载#3数据到操作数栈istore_2
:将操作数栈顶数据弹出,存入局部变量表的slot 2iload_1
:从局部变量表slot 1中取出数据,存入操作数栈中iload_2
iadd
:将操作数栈中的数据相加istore_3
:从操作数栈顶将数据弹出,存入局部变量表的slot 3getstatic #4
iload_3
invokevirtual #5
:找到常量池 #5 项,定位到方法区java/io/PrintStream.println:(I)V
方法,生成新的栈帧(分配locals、stack等),传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除main操作数栈的内容
加载和存储指令
- 将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
- 将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
- 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
- 扩充局部变量表的访问索引的指令:wide
运算指令
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 取反指令:ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位与指令:iand、land
- 按位异或指令:ixor、lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
对象创建与访问指令
- 创建类实例的指令:new
- 创建数组的指令:newarray、anewarray、multianewarray
- 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
操作数栈管理指令
- 将操作数栈的栈顶一个或两个元素出栈:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 将栈最顶端的两个数值互换:swap
控制转移指令
- 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
方法调用和返回指令
- invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface指令:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic指令:用于调用类静态方法(static方法)。
- invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法。并执行该方法。前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
二、语法糖
默认构造器
public class Candy1 {
}
编译成class文件后的代码:
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
自动装拆箱
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
该特性事在JDK 5 后加入的,工作原理如下:
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
泛型擦除
Java在编译后会执行泛型擦除动作,实际类型都被当成Object类型处理,因此在取值时,就需要额外进行类型转换操作
public class Candy3 {
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类型,则需要进行拆箱操作
int x = ((Integer)list.get(0)).invalue();
可变参数
public class Candy4 {
public static void foo(String... args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("hello", "world");
}
}
在编译期间,可变参数会转换成一个数组
public class Candy4 {
public static void foo(String[] args) {
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{
"hello", "world"});
}
}
foreach循环
- 普通foreach循环
public class Candy5_1 { public static void main(String[] args) { int[] array = { 1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦 for (int e : array) { System.out.println(e); } } }
public class Candy5_1 { public Candy5_1() { } 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 class Candy5_2 { 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 class Candy5_2 { public Candy5_2() { } 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); } } }
switch字符串
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}
编译时,会进行两重校验,对需要case的内容转换成hashcode,进行比较;而进行equals时,是为了防止hashcode冲突
public class Candy6_1 {
public Candy6_1() {
}
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");
}
}
}
匿名内部类
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);
}
}
三、类加载
- 加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
- 验证:确保Class文件的字节流中包含的信息符合规范
- 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
- 元数据验证:对字节码描述的信息进行语义分析
- 字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
- 符号引用验证:对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验
- 准备:正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值
- 这些变量所使用的内存都应当在方法区中进行分配
- 进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
- 解析:Java虚拟机将常量池内的符号引用替换为直接引用的过程
- 类或者接口的解析
- 字段解析
- 方法解析
- 接口方法解析
- 初始化:初始化阶段就是执行类构造器
<clinit>()
方法的过程<clinit>()
方法是由编译器自动收集类中所有的类变量的赋值动作和静态语句块中的语句合并产生<clinit>()
不需要显式地调用父类构造器,Java虚拟机会确保父类<clinit>()
在子类<clinit>()
之前执行
class A { static int a = 0; static { System.out.println("a init"); } } class B extends A { final static double b = 5.0; static boolean c = false; static { System.out.println("b init"); } } public class Load3 { static { System.out.println("main init"); } public static void main(String[] args) throws ClassNotFoundException { // 1. 静态常量(基本类型和字符串)不会触发初始化 System.out.println(B.b); // 2. 类对象.class 不会触发初始化 System.out.println(B.class); // 3. 创建该类的数组不会触发初始化 System.out.println(new B[0]); // 4. 不会初始化类 B,但会加载 B、A ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl.loadClass("cn.itcast.jvm.t3.B"); // 5. 不会初始化类 B,但会加载 B、A ClassLoader c2 = Thread.currentThread().getContextClassLoader(); Class.forName("cn.itcast.jvm.t3.B", false, c2); // 1. 首次访问这个类的静态变量或静态方法时 System.out.println(A.a); // 2. 子类初始化,如果父类还没初始化,会引发 System.out.println(B.c); // 3. 子类访问父类静态变量,只触发父类初始化 System.out.println(B.a); // 4. 会初始化类 B,并先初始化类 A Class.forName("cn.itcast.jvm.t3.B"); } }
四、类加载器
双亲委派模型
- 如果一个类加载器收到了类加载的请求,它首先将请求委派给父类加载器去完成,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。