JVM_09 类加载与字节码技术(字节码指令2)

1.条件判断指令

 几点说明

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

 源码:

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

字节码:

0: iconst_0
1: istore_1
2: iload_1   
3: ifne     12
6: bipush    10
8: istore_1
9: goto     15
12: bipush    20
14: istore_1
15: return

思考
以上比较指令中没有 long,float,double 的比较,那么它们要比较怎么办?
参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

  循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环:

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

 字节码是:

0: iconst_0
1: istore_1
2: iload_1
3: bipush    10
5: if_icmpge   14
8: iinc     1, 1
11: goto     2
14: return

再比如 do while 循环:

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

字节码是:

0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return

最后再看看 for 循环:

public class Demo3_6 {
  public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
   }
 }
}

字节码是:

0: iconst_0
1: istore_1
2: iload_1
3: bipush    10
5: if_icmpge   14
8: iinc     1, 1
11: goto     2
14: return

注意
比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归。

 练习 - 判断结果

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

public class Demo3_6_1 {
  public static void main(String[] args) {
    int i = 0;
    int x = 0;
    while (i < 10) {
      x = x++;
      i++;
   }
    System.out.println(x); // 结果是 0
 }
}

这里先进行iload_x ,操作栈里面存放一个0,局部变量表中的x变为1, 在进行赋值操作,将操作栈里面的0赋值给了局部变量表里面的x,这时候局部变量表里面的x变为0。所以最后x还是为0。

  构造方法

1)<cinit>()V

public class Demo3_8_1 {
  static int i = 10;
  static {
    i = 20;
 }
  static {
    i = 30;
 }
}

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

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

所以最后的i=30;这里会将i不断地覆盖掉。 

<cinit>()V 方法会在类加载的初始化阶段被调用

2) <init>()V

public class Demo3_8_2 {
  private String a = "s1";
 {
    b = 20;
 }
  private int b = 10;
 {
    a = "s2";
 }

  public Demo3_8_2(String a, int b) {
    this.a = a;
    this.b = b;
 }
  public static void main(String[] args) {
    Demo3_8_2 d = new Demo3_8_2("s3", 30);
    System.out.println(d.a);
    System.out.println(d.b);
 }
}

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构
造方法内的代码总是在最后,所以上述d.a=s3,d.b=30。

public hubin.Demo3_8_2(java.lang.String, int);
  descriptor: (Ljava/lang/String;I)V
  flags: ACC_PUBLIC
  Code:
   stack=2, locals=3, args_size=3
    0: aload_0
    1: invokespecial #1   // super.<init>()V
    4: aload_0
    5: ldc      #2   // <- "s1" 
    7: putfield    #3   // -> this.a
    10: aload_0
    11: bipush     20   // <- 20
    13: putfield    #4   // -> this.b
    16: aload_0
    17: bipush     10   // <- 10
    19: putfield    #4   // -> this.b
    22: aload_0
    23: ldc      #5   // <- "s2"
    25: putfield    #3   // -> this.a
    28: aload_0       // ------------------------------
    29: aload_1       // <- slot 1(a) "s3"      |
    30: putfield    #3   // -> this.a          |
    33: aload_0                       |
    34: iload_2       // <- slot 2(b) 30       |
    35: putfield    #4   // -> this.b --------------------
    38: return
   LineNumberTable: ...
   LocalVariableTable:
    Start  Length  Slot  Name  Signature
      0    39   0  this  Lhubin/Demo3_8_2;
      0    39   1   a  Ljava/lang/String;
      0    39   2   b  I
  MethodParameters: ...

方法调用

public class Demo3_9 {
  public Demo3_9() { }
  private void test1() { }
  private final void test2() { }
  public void test3() { }
  public static void test4() { }
  public static void main(String[] args) {
    Demo3_9 d = new Demo3_9();
    d.test1();
    d.test2();
    d.test3();
    d.test4();
    Demo3_9.test4();
 }
}

 字节码:

0: new      #2         // class hubin/Demo3_9
3: dup
4: invokespecial #3         // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4         // Method test1:()V
12: aload_1
13: invokespecial #5         // Method test2:()V
16: aload_1
17: invokevirtual #6         // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7         // Method test4:()V
25: invokestatic #7         // Method test4:()V
28: return

new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配
合 invokespecial 调用该对象的构造方法 "<init>":()V (会消耗掉栈顶一个引用),另一个要
配合 astore_1 赋值给局部变量
最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于
态绑定

普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用
invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
还有一个执行 invokespecial 的情况是通过 super 调用父类方法

 多态的原理

package hubin;
import java.io.IOException;
/**
* 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
* -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
*/
public class Demo3_10 {
  public static void test(Animal animal) {
    animal.eat();
    System.out.println(animal.toString());
 }
  public static void main(String[] args) throws IOException {
    test(new Cat());
    test(new Dog());
    System.in.read();
 }
}
abstract class Animal {
  public abstract void eat();
  @Override
  public String toString() {
    return "我是" + this.getClass().getSimpleName();
 }
}
class Dog extends Animal {
  @Override
  public void eat() {
    System.out.println("啃骨头");
 }
}
class Cat extends Animal {
  @Override
  public void eat() {
    System.out.println("吃鱼");
 }
}

 1)运行代码
停在 System.in.read() 方法上,这时在终端上运行 jps 获取进程 id
2)运行 HSDB 工具
进入 JDK 安装目录,执行:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
进入图形界面 attach 进程 id
3)查找某个对象
打开 Tools -> Find Object By Query
输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 点击 Execute 执行

 4)查看对象内存结构
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是
MarkWord,后 8 字节就是对象的 Class 指针
但目前看不到它的实际地址

 

 

 5)查看对象 Class 的内存地址
可以通过 Windows -> Console 进入命令行模式,执行
mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)
结果中第二行 0x14c330a0即为 Class 的内存地址

 

 

 6)查看类的 vtable
Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面

 

 可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态
相关的,final,static 不会列入)
那么这 6 个方法都是谁呢?从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计
算得到:

0x000000001b7d4028
       1b8 +
---------------------       
0x000000001b7d41e0

 通过 Windows -> Console 进入命令行模式,执行

mem 0x000000001b7d41e0 6
0x000000001b7d41e0: 0x000000001b3d1b10
0x000000001b7d41e8: 0x000000001b3d15e8
0x000000001b7d41f0: 0x000000001b7d35e8
0x000000001b7d41f8: 0x000000001b3d1540
0x000000001b7d4200: 0x000000001b3d1678
0x000000001b7d4208: 0x000000001b7d3fa8

 就得到了 6 个虚方法的入口地址

7)验证方法地址
通过 Tools -> Class Browser 查看每个类的方法定义,比较可知

Dog - public void eat() @0x000000001b7d3fa8
Animal - public java.lang.String toString() @0x000000001b7d35e8;
Object - protected void finalize() @0x000000001b3d1b10;
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8;
Object - public native int hashCode() @0x000000001b3d1540;
Object - protected native java.lang.Object clone() @0x000000001b3d1678;

 对号入座,发现

  • eat() 方法是 Dog 类自己的
  • toString() 方法是继承 String 类的
  • finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的

8)小结
当执行 invokevirtual 指令时,

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

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值