从 Java 字节码分析中我学到了什么

2020-06-05 00:00:00 变量 调用 引用 方法 常量

实参和形参

public int sum(int x,int y) {
return x+y;
}

sum(2,3);

上面的代码中 sum() 方法中的 x,y 就是形参, 而调用方法 sum(2,3) 中的 2 与 3 就是实参。形参是在方法定义阶段,而实参实在方法调用阶段。

查看字节码

基本类型参数调用

private static int intStatic = 222;

public static void main(String[] args) {
method(intStatic);
System.out.println(intStatic);
}

public static void method(int intStatic) {
intStatic = 777;
}

上面的 method() 方法的字节码 (javap -verbose XXX.class) 如下:

public static void method(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: sipush 777
3: istore_0
4: return
LineNumberTable:
line 13: 0
line 14: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 intStatic I



sipush: 将一个短整形常量值推送至栈顶

iconst 将 int 形式 - 1-5 推送到栈顶
bipush 表示将单字节的常量值 (-128-127) 推送至栈顶
sipush 表示一个短整形常量值 (-32768-32767) 推送至栈顶

istore_0: 将栈顶 int 型数值存入个本地变量。

上面字节码的意思就是将 777 推送到栈顶,然后将其赋值给 intStatic 这个本地变量。所以我们输出的结果是 222, 因为 method() 中的赋值是对本地变量进行赋值的, 并没有改变 static 变量的值, 这也是 Java 的变量就近原则, 当然可以使用 Class.intStatic 这样的方式显示声明。

不可变对象参数调用

private static String stringStatic = "old string";

public static void main(String[] args) {
method(stringStatic);
System.out.println(stringStatic);
}
public static void method(String stringStatic) {
stringStatic = "new string";
}

上面输出的结果是old string, 同样反编译看下字节码

Constant pool:
#6 = String #35 // new string
#7 = String #36 // old string
#10 = Utf8 stringStatic
#35 = Utf8 new string
#36 = Utf8 old string

public static void method(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: ldc #6 // String new string
2: astore_0
3: return
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 stringStatic Ljava/lang/String;


ldc: 将 int、float 或 String 型常量值从常量池中推送至栈顶。
astore_0: 将栈顶引用型数值存入个本地变量。

字节码的意思是将 #6(符号引用) 的值推送至栈顶, 然后将其引用型数值赋值给个本地变量 (stringStatic)。

可以看到这里的 #6 表示是一个 String 类型数据,它指向常量池中一个 CONSTANT_Utf8_info(缩写 Utf8,Class 文件中方法字段等都需要引用它来描述名称)类型,这个常量代表了类 (或者接口) 的全限定名称, 在运行的时候,JVM 会根据这个全限定名称来实例化这个类, 那个时候符号引用会被转换为直接引用(就是内存中的地址)。

运行时常量池

上面一直在说常量池, 那么常量池到底是个什么东东?

我们都知道方法区与 Java 堆一样, 是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池 (Constant Pool Table),用于存放编译期产生的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有在编译器才能产生,也就是并非预置入 Class 文件常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中, String.intern() 方法就是典型的代表。

代和方法区,很多人会迷惑。方法区是 JVM 规范, 而代只是实现。
从 JDK7 开始常量池已经从方法区移动到堆中

文本字符串、声明为 final 的常量值都为字面量,而符号引用包含了下面三类常量

1. 类和接口的全限定名
2. 字段的名称和描述符
3. 方法的名称和描述符
Constant pool:
#1 = Methodref #9.#28 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#29 // com/generalthink/kafka/ParamDemo.stringStatic:Ljava/lang/String;
#3 = Methodref #8.#30 // com/generalthink/kafka/ParamDemo.method:(Ljava/lang/String;)V
#8 = Class #37 // com/generalthink/kafka/ParamDemo


所以, 编译器符号引用 stringStatic 和字面量 old string 会被加入到 Class 文件的常量池中,然后在类加载阶段,这两个常量会进入运行时常量池。

可变对象参数调用

上面的参数传递的是不可变对象,这里变成可变对象我们再次分析下

private static StringBuilder stringBuilderStatic = new StringBuilder("old stringBuilder");

public static void main(String[] args) {
method(stringBuilderStatic);
System.out.println(stringBuilderStatic);
}

public static void method(StringBuilder stringBuilderStaticParam) {
stringBuilderStaticParam.append(" first append");

stringBuilderStaticParam = new StringBuilder("new stringBuilder");
stringBuilderStaticParam.append(" new method's append");
}

查看对应的关键字节码如下:

Constant pool:
#6 = String #41 // first append
#8 = Class #43 // java/lang/StringBuilder
#9 = String #44 // new stringBuilder
#11 = String #46 // new method's append
#12 = String #47 // old stringBuilder
#15 = Utf8 stringBuilderStatic
#30 = Utf8 stringBuilderStaticParam
#41 = Utf8 first append
#43 = Utf8 java/lang/StringBuilder
#44 = Utf8 new stringBuilder
#46 = Utf8 new method's append
#47 = Utf8 old stringBuilder

public static void method(java.lang.StringBuilder);
descriptor: (Ljava/lang/StringBuilder;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: ldc #6 // String first append
3: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
6: pop
7: new #8 // class java/lang/StringBuilder
10: dup
11: ldc #9 // String new stringBuilder
13: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
16: astore_0
17: aload_0
18: ldc #11 // String new method's append
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: return
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 stringBuilderStaticParam Ljava/lang/StringBuilder;


aload_0: 将个引用类型本地变量推送至栈顶。
ldc : 将 int、float 或 String 型常量值从常量池中推送至栈顶。
invokevirtual:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
pop: 将栈顶数值弹出 (不能是 long 或 double 类型)
new: 创建一个对象, 并将其引用值压入栈顶
dup:复制栈顶数值并将复制值压入栈顶
astore_0 : 将栈顶引用型数值存入个本地变量
return : 从当前方法返回 void

需要注意的是 aload_0 中的 0 指的是LocalVariableTable中 slot 为 0 的参数,这里指的是 stringBuilderStaticParam。它是把静态变量的引用赋值给虚拟机栈帧中的局部变量表。

上面字节码的意思就是 stringBuilderStaticParam 推送至栈顶, 然后将 first append 常量值推送到栈顶,调用 StringBuilder.append 方法得到结果, 后出栈。
接着 new 一个 StringBuilder, 将返回的地址复制一份压入栈顶, 然后在将这个地址存入 stringBuilderStaticParam。然后重新 aload 到操作栈顶 (这里的值已经被进行了覆盖, 所以后续对于 stringBuilderStaticParam 的 append 操作与类的静态变量 stringBuilderStatic 没有任何关系), 然后接着调用 append 方法, 后返回 void。

需要注意的是 stringBuilderStatic 仅仅只是一个指针, 一个指向内存中具体地址的指针而已, 它并不是这个内存地址。java spec 中声明说,java 中的所有东西都是值传递, 从没有引用传递这个玩意儿
代码是检验整理的标准, 现在假设是引用传递, 那么执行 method 方法之后, 输出的结果就应该是new stringBuilder new method's append, 但是输出结果并不是,所以参数传递是值传递, 这个值对对象来说是指针而已。

this 是如何实现的

int m = 1;
public static void main(String[] args) {
ParamDemo demo = new ParamDemo();
System.out.println(demo.method());
}
public int method() {
return m + 1;
}

我们在 method() 方法中调用了 m, 这里隐式的使用了 this, 其实是 this.m。那么 this 式如何实现的呢?

public int method();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/generalthink/kafka/ParamDemo;
}


我们在 LocalVariableTable 中可以发现 this 成为了一个参数,它的实现就是这样简单,Javac 编译器编译的时候把 this 关键字的访问转变为对一个普通方法参数的访问, 然后在虚拟机调用实例方法时自动传入此参数。

一说到 this 肯定就会想起 super, 其实 super 就是一个普通的方法调用, 通过 invokespecial 指令实现。

String.intern() 原理

当使用 intern() 方法的时候你要想到方法作用是将字面量动态的加入运行时常量池。如果运行时常量池中已经存在了相同的字符串 (equals 方法决定), 则返回池中的对象,否则将其加入到常量池后返回对应的引用。

String s1 = "Hello";
String s2 = new String("Hello").intern();

//true
System.out.println(s1 == s2);

输出结果为 true, 编译期间”Hello” 这个字面量已经加入到了常量池, 运行期间, 调用了 intern() 方法, 先根据 equals 方法判断两个字符串相等, 然后返回常量池当中 Hello 的引用地址, 所以此时 s1,s2 其实指向的是同一个地址。

我们将代码做一点修改

String s1 = "Hello";
String s2 = "World";

String s3 = s1 + s2;

String s4 = "Hello" + "World";

System.out.println(s3 == s4);

System.out.println(s3.intern() == s4);

输出结果分别为 false 和 true。我们注意到 s3 和 s4 多的不同就是, s3 是由变量相加得到的, 同样查看字节码

#2 = String     #37     // Hello
#3 = String #38 // World
#4 = Class #39 // java/lang/StringBuilder
#5 = Methodref #4.#36 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#40 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #4.#41 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = String #42 // HelloWorld

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: ldc #2 // String Hello
2: astore_1
3: ldc #3 // String World
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: ldc #8 // String HelloWorld
27: astore 4
...

从字节码可以看出来 s3 实际上是调用了 StringBuilder.append() 方法来得到的, 而 s4 的值在编译的时候就可以直接确定, 它是一个准确的值, 所以此时 HelloWorld 在常量池中就存在了。

相关文章