從JVM Instructions看Java


我們都知道Java程序是運行在JVM裏面的一段一段字節碼,JVM需要做的就是把這些字節碼轉換成機器語言,使得Java程序能正確的運行在計算機上,說的更底層一點就是正確分配內存,執行CPU計算並且釋放內存。所以任何一個程序如果能做到以下幾件事:讀入Java Class文件、分析Class文件格式、爲變量對象方法動態分配內存、管理這些變量和內存的回收,都可以做爲我們所謂的虛擬機爲Java程序員服務。從這個意義上來說,JVM好似一個Java程序和機器語言的中間轉換裝置。

        要實現轉換,JVM並不是一步到位的,程序員看到的Java源代碼是最接近人類語言的可讀性較高,便於我們設計程序的功能和邏輯。之後經過編譯這些java文件轉換成了class文件,內容全都是字節碼,這些字節碼被JVM認識,有的字節是變量名,有的字節是變量值,有的字節是JVM指令集中的指令,就如同彙編語言一樣,這些字節碼按照一定的順序組合起來,一個指令後面會跟着固定數量的操作數。JVM也會維護內存中的一些棧結構,不斷的push和pop引用的地址或基本類型變量的值。我們可以通過研究一下JVM的指令集幫助我們理解Java語言,或者有時還可以幫助我們分析程序的性能。

        先寫一個簡單的Java程序:

  1. import java.util.*;

  2. public class Demo{
  3.     private static double d = 3.14;

  4.     public static void main(String[] args){
  5.         List<String> list = new ArrayList<String>();

  6.         list.add("Hello");
  7.         int size = list.size();

  8.         String s = null;
  9.         if(size>0)
  10.             s = list.get(0);

  11.         if(s!=null)
  12.             s+=" World!";

  13.         System.out.println(d);
  14.         System.out.println(s);
  15.     }
  16. }

        這裏用到一些基本語法,例如接口簽名、整型賦值、String構造、String操作等。之後我們執行javac Demo.java編譯成Demo.class文件,JDK提供了一個反編譯指令集命令javap,執行javap -c Demo > Instructions.txt會解析class文件,排版針對Demo.class文件生成JVM字節碼,再來看看這個字節碼吧:

  1. Compiled from "Demo.java"
  2. public class Demo extends java.lang.Object{
  3. public Demo();
  4.   Code:
  5.    0:   aload_0
  6.    1:   invokespecial   #1//Method java/lang/Object."<init>":()V
  7.    4:   return

  8. public static void main(java.lang.String[]);
  9.   Code:
  10.    0:   new #2//class java/util/ArrayList
  11.    3:   dup
  12.    4:   invokespecial   #3//Method java/util/ArrayList."<init>":()V
  13.    7:   astore_1
  14.    8:   aload_1
  15.    9:   ldc #4//String Hello
  16.    11:  invokeinterface #5,  2//InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
  17.    16:  pop
  18.    17:  aload_1
  19.    18:  invokeinterface #6,  1//InterfaceMethod java/util/List.size:()I
  20.    23:  istore_2
  21.    24:  aconst_null
  22.    25:  astore_3
  23.    26:  iload_2
  24.    27:  ifle    41
  25.    30:  aload_1
  26.    31:  iconst_0
  27.    32:  invokeinterface #7,  2//InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
  28.    37:  checkcast   #8//class java/lang/String
  29.    40:  astore_3
  30.    41:  aload_3
  31.    42:  ifnull  65
  32.    45:  new #9//class java/lang/StringBuilder
  33.    48:  dup
  34.    49:  invokespecial   #10//Method java/lang/StringBuilder."<init>":()V
  35.    52:  aload_3
  36.    53:  invokevirtual   #11//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  37.    56:  ldc #12//String  World!
  38.    58:  invokevirtual   #11//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  39.    61:  invokevirtual   #13//Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  40.    64:  astore_3
  41.    65:  getstatic   #14//Field java/lang/System.out:Ljava/io/PrintStream;
  42.    68:  getstatic   #15//Field d:D
  43.    71:  invokevirtual   #16//Method java/io/PrintStream.println:(D)V
  44.    74:  getstatic   #14//Field java/lang/System.out:Ljava/io/PrintStream;
  45.    77:  aload_3
  46.    78:  invokevirtual   #17//Method java/io/PrintStream.println:(Ljava/lang/String;)V
  47.    81:  return

  48. static {};
  49.   Code:
  50.    0:   ldc2_w  #18//double 3.14d
  51.    3:   putstatic   #15//Field d:D
  52.    6:   return

  53. }

        有了這一段字節碼,我們就可以初步開始認識JVM的指令了,每個指令都有一個對應的字節碼,我就順便在講解中稍微標註幾個在指令後的括號裏方便大家理解,首先看到的是aload_0(字節碼:0x19),表示從執行棧的下標0出讀取一個引用出來,接着是invokespecial(字節碼:0xb7),官方解釋:Invoke the method, special handling for superclass, private, and instance initialization method invocations。這裏要區別於下面出現的invokevirtual指令(字節碼:0xb6),它們的區別也就是invokevirtual更加通用一點,會調用一個實例的方法,而invokespecial是定製針對三種情況下用的:

        1、私有方法

        2、調用父類繼承下來的方法

        3、每個對象的初始化

        所以<init>動作用於創建對象時進行初始化,當在JVM Heap中創建對象時,一旦在Heap中分配了空間,最先就會調用"<init>"方法,包括實例變量的賦值和初始化靜態塊等。#number指明瞭在操作棧中各個變量的下標。其他指令比如dup是賦值整個操作棧,pop彈出操作棧頂層值,ldc是指Push item from runtime constant pool,即從常量池中取值壓入操作棧中。ifle和ifnull是判斷分支語句,分別表示小於和是否是null,後面跟着的是判斷成功的話當前指針應該跳轉的偏移量。關於offset(偏移量)相信學過C/C++或者彙編的理解起來更輕鬆一些。

 

        其他指令就不一一解釋了,有一點小規律是關於int類型的操作指令一般都以i開頭,類似的還有float和double、array、char、short類型等。這裏我特意做了一個String類型的+=操作,通過反編譯指令我們可以看到JVM內部對於程序中的"Hello"和" World!"都是從常量池中去出來的(ldc指令的含義),然後JVM構造了一個StringBuilder(早期的JDK1.4則應該是線程安全但效率較的低StringBuffer),通過這個類來實現String的連接+=操作,每次+=返回的都是一個新的String對象。所以我們推薦在大量操作String的時候,直接使用StringBuilder這個類。(記住不是線程安全限制更多的StringBuffer哦)

        在一些J2EE應用中,熟悉這些指令也能幫助我們深入到框架內部,比如JPA提出對實體映射類進行的Enhance就會直接改變class文件的字節碼,我們可以通過反編譯觀察這些指令來幫助我們分析一些利用動態代理或者Instrument接口實現的對Class字節碼進行的修改到底原理是怎麼樣?性能又如何?

        關於JVM指令集全部說明可以參見官方文檔:http://java.sun.com/docs/books/vmspec/index.html


本文出自:http://blog.csdn.net/ant_yan/article/details/2903630

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章