java 程序運行的基礎知識【Java bytecode】

聊聊文字,寫一篇關於 java 基礎知識的博文。java

JVM 線程棧 到 函數運行

每個JVM線程來講啓動的時候都會建立一個私有的線程棧。一個jvm線程棧用來存儲棧幀,jvm線程棧和C語言中的棧很相似,它負責管理局部變量、部分運算結果,同時也參與到函數調用和函數返回的工做中。JVM規範中運行線程棧的大小能夠是固定的或者是動態分配的,也能夠是根據必定規則計算的。不一樣jvm對棧的實現會不一樣,一些可能提供給開發人員本身控制jvm線程棧初始大小的方式;對於動態分配來講也可能提供對jvm最大和最小值的設置。node

當計算一個線程須要的分配的大小超出了固定值、或者設置的最大值,jvm會拋出StackOverflowError。而對於動態分配棧來講,若是內存不可以提供足夠的空間來知足最小值、或者須要的值JVM會拋出 OutOfMemoryErrorjson

棧幀,能夠理解成一個函數執行的環境,它管理參數、局部變量、返回值等等。數組

每一個棧幀都包括一個管理局部變量的數組( 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入棧中完成指令。app

除開以上兩個關鍵的結構,每一個棧幀還有常量池( run-time constant pool)、異常拋出管理等結構。在此就不一一詳細說來了,能夠參考其餘資料。框架

再來經過一個簡單的 Demo 來講明,一個棧幀的工做。首先,咱們來看這樣的一個函數:jvm

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 來編譯這個類的,而後用 javap -c -v Hello.class 來解析編譯的class。

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;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

magic: 就是很是有名的 0xCAFEBABE ,一個標識class文件;

minor_version 、major_version :指的是java class 文件的版本,通常說class文件的版本是 XX.xx 其中XX 就是major,xx是minor,好比上面demo中的版本是52.0 表明就是 minor 0,major 51.

constant_pool_count:就是常量池元素個數,cp_info constant_pool[constant_pool_count-1] 就是相關的詳細信息了。

access_flags:指的是訪問標識例如ACC_PUBLIC、ACC_FINAL、ACC_INTERFACE、ACC_SUPER 寫過java的相信看名字應該知道啥意思,ACC是access的縮寫。

其餘具體的,就不一一介紹了詳細能夠直接參考官方文檔。

動態生成java字節碼

固然,你能夠直接按照官方的class文件格式來直接寫 byte[],而後自定義個 class load 載入編寫的byte[]來實現動態生成class。不過,這個要求可能也有點高,必須的很是熟悉class文件格式才能作到。這裏demo仍是藉助 ASM 這個類庫來簡單演示下,就編寫下 上面的Hello 不過裏面只實現say的方法。以下:

public class AsmDemo {

    public static final String CLASS_NAME = "Hello";    
    public static final AsmDemoLoad load = new AsmDemoLoad();

    private static class AsmDemoLoad extends ClassLoader {

        public AsmDemoLoad() {

            super(AsmDemo.class.getClassLoader());
        }

        public Class<?> defineClassForName(String name, byte[] data) {
            return this.defineClass(name, data, 0, data.length);
        }
    }

    public static byte[] generateSayHello() throws IOException {

        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        classWriter.visit(V1_7, ACC_PUBLIC + ACC_SUPER, CLASS_NAME, null, getInternalName(Object.class), null);
    
        //默認初始化函數
        Method constructorMethod = Method.getMethod("void <init> ()");
        GeneratorAdapter constructor = new GeneratorAdapter(ACC_PUBLIC, constructorMethod, null, null, classWriter);
        constructor.loadThis();
        //每一個類都要基礎Object
        constructor.invokeConstructor(Type.getType(Object.class), constructorMethod);
        constructor.returnValue();
        constructor.endMethod();

        Method mainMethod = Method.getMethod("void say ()");
        GeneratorAdapter main = new GeneratorAdapter(ACC_PUBLIC, mainMethod, null, null, classWriter);
        main.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
        main.push("Hello world!");
        main.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println (String)"));
        main.returnValue();
        main.endMethod();

        return classWriter.toByteArray();
    }

    public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException,
            InvocationTargetException, InstantiationException, NoSuchMethodException, SecurityException, IOException {

        byte[] code = AsmDemo.generateSayHello();

        //反射構建 hello 類,調用hello方法。
        Class<?> hello = load.defineClassForName(CLASS_NAME, code);
        hello.getMethod("say", null).invoke(hello.newInstance(), null);
    }
}

關於動態生成字節碼用途,必定場景下是能夠提高效率與性能,由於動態生成的類和普通的載入類並沒有太大區別。手工優化後的字節碼執行可能比編譯的要優,能夠替代反射使用的許多場景 同時避免反射的性能消耗。很著名的一個例子,fastJSON 就是使用內嵌 ASM 框架動態生成字節碼類,來進行序列和反序列化工做,是目前公認最快的json字符串解析。

相關文章
相關標籤/搜索