一篇 java 程序运行的基础知识【Java bytecode】

 

JVM 线程栈 到 函数运行

每一个JVM线程来说启动的时候都会创建一个私有的线程栈。一个jvm线程栈用来存储栈帧,jvm线程栈和C语言中的栈很类似,它负责管理局部变量、部分运算结果,同时也参与到函数调用和函数返回的工作中。JVM规范中运行线程栈的大小可以是固定的或者是动态分配的,也可以是根据一定规则计算的。不同jvm对栈的实现会不同,一些可能提供给开发人员自己控制jvm线程栈初始大小的方式;对于动态分配来说也可能提供对jvm最大和最小值的设置。

当计算一个线程需要的分配的大小超出了固定值、或者设置的最大值,jvm会抛出StackOverflowError。而对于动态分配栈来说,如果内存不能够提供足够的空间来满足最小值、或者需要的值JVM会抛出 OutOfMemoryError

栈帧,可以理解成一个函数执行的环境,它管理参数、局部变量、返回值等等。

每个栈帧都包括一个管理局部变量的数组( local variables),这个数组的单元数量在编译成字节码的时候就能确定了。对于32-bit 一个单位能够存放 boolean, byte, char, short, int, float, reference,returnAddress;连续两个单位就能够用来存储long 、double。局部变量数组的下标是从0开始,一般而言0位置存储的是this,后面接着是函数的参数,再是函数中出现的局部变量。

每个栈帧也都包括一个(LIFO)操作栈的数据结构(operand stack),它的大小同样也可以在编译的时候确定,创建的时候会是个空栈。举个简单的例子,来描述它公用,对于int a+b来说,先把push a 进入栈中,再朴实 b 进入入栈中,然后 同时pop 两个值执行iadd 指令,再将其加后的结果push入栈中完成指令。

除开以上两个关键的结构,每个栈帧还有常量池( run-time constant pool)、异常抛出管理等结构。在此就不一一详细说来了,可以参考其他资料。

再来通过一个简单的 Demo 来说明,一个栈帧的工作。首先,我们来看这样的一个函数:
public int comp(float number1, float number2){
        int result ;
        if(number1 < number2)
            result = 1;
        else
            result = 2;
        return result;
    }
其中函数内逻辑对应的字节码,如下:
0: fload_1
 1: fload_2
 2: fcmpg
 3: ifge          11
 6: iconst_1
 7: istore_3
 8: goto          13
11: iconst_2
12: istore_3
13: iload_3
14: ireturn
对于这几个字节码指令稍微说明下:
fload_x:取局部变量数组中第x个,类型fload,push 入栈;
fcmpg:比较两个单精度浮点数。如果两数大于结果为1,相等则结果为0,小于的话结果为-1;
ifge:跳转指令;
iconst_x:push 常量x入栈;
istore_x:pop栈存入局部变量数组第x个;
iload_x:读取局部变量数组第x个,入栈;
ireturn:函数结束返回int型;
细心点观察可以发现i开头指代int,f开头指代fload,load代表载入,if代表跳转等等,其中字节码的操作码定义也是有一定意义的,详情可以翻译jvm字节码相关标准。再来看看,jvm如何在栈帧结构上执行情况,以具体调用comp(1.02,2.02)为例: 
 
Java 的 Class

说字节码,一定少不了.class。不妨,以一个demo类 来具体看class 的内容,类非常简单,两个函数一个say,另外一个就是上面的cmp函数。
public class Hello {

    public void say(){
        System.out.println("Hello world!");
    }

    public int comp(float number1, float number2){
        int result ;
        if(number1 < number2)
            result = 1;
        else
            result = 2;
        return result;
    }
}
用 javac -g:none Hello.java 来编译这个类的,然后用
Classfile /src/main/java/com/demo/Hello.class
  Last modified 2016-10-28; size 404 bytes
  MD5 checksum 9ac6c800c312d65b568dd2a0718bd2c5
public class com.demo.Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #17            // Hello world!
   #4 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #20            // com/demo/Hello
   #6 = Class              #21            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               say
  #11 = Utf8               comp
  #12 = Utf8               (FF)I
  #13 = Utf8               StackMapTable
  #14 = NameAndType        #7:#8          // "<init>":()V
  #15 = Class              #22            // java/lang/System
  #16 = NameAndType        #23:#24        // out:Ljava/io/PrintStream;
  #17 = Utf8               Hello world!
  #18 = Class              #25            // java/io/PrintStream
  #19 = NameAndType        #26:#27        // println:(Ljava/lang/String;)V
  #20 = Utf8               com/demo/Hello
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/System
  #23 = Utf8               out
  #24 = Utf8               Ljava/io/PrintStream;
  #25 = Utf8               java/io/PrintStream
  #26 = Utf8               println
  #27 = Utf8               (Ljava/lang/String;)V
{
  public com.demo.Hello();
    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

  public void say();
    descriptor: ()V
    flags: ACC_PUBLIC
    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

  public int comp(float, float);
    descriptor: (FF)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: fload_1
         1: fload_2
         2: fcmpg
         3: ifge          11
         6: iconst_1
         7: istore_3
         8: goto          13
        11: iconst_2
        12: istore_3
        13: iload_3
        14: ireturn
      StackMapTable: number_of_entries = 2
        frame_type = 11 /* same */
        frame_type = 252 /* append */
          offset_delta = 1
          locals = [ int ]
}
解释下其中涉及的新的操作码
getstatic:获取镜头变量;
invokevirtual:调用函数;
return:void 函数结束返回;
在 public int comp(float, float) code 这段代码里面就能看到上面提到的字节码运行的例子。有了个感性认识,其实大体看到class文件里面,除了字节码指令外,还包括了常量pool,访问标志(public 等),类的相关信息(属性、函数、常量等)。因为前面用的是 -g:node进行编译的,其他模式下还可以有其他扩展、调试信息也包括在class里面。官方给出的class文件格式,详细如下:
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;
 &

50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信