01 一些關於java編譯器的問題(init, clinit的生成, 自己實現javap?)

前言 

呵呵 最近看到了一系列跟 java編譯器 相關的一系列的問題, 所以整理了一下 

一下部分代碼, 截圖 基於 : jdk7u40, idea2019 的 bytecode viewer, jls7, jdk7 的 javac 

 

 

1. 關於 javap 裏面看不到 "<init>", "<clinit>" 

https://hllvm-group.iteye.com/group/topic/35224

看到這篇文章的時候, 去搜索了一下 javap 對應的代碼 
我這裏搜到的結果和 R大 的回答似乎是不一致的, 可能是看的代碼的版本不一樣吧, 呵呵

com.sun.tools.javap.ClassWriter. writeMethod 

        writeModifiers(flags.getMethodModifiers());
        if (methodType != null) {
            writeListIfNotEmpty("<", methodType.typeParamTypes, "> ");
        }
        if (getName(m).equals("<init>")) {
            print(getJavaName(classFile));
            print(getJavaParameterTypes(d, flags));
        } else if (getName(m).equals("<clinit>")) {
            print("{}");
        } else {
            print(getJavaReturnType(d));
            print(" ");
            print(getName(m));
            print(getJavaParameterTypes(d, flags));
        }

 

測試代碼如下

/**
 * Test11InitAndClinit
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2019/11/16 17:00
 */
public class Test11InitAndClinit {

    // x
    private int x = 1;

    public Test11InitAndClinit() {
        int x = 4;
    }

    public Test11InitAndClinit(int arg) {
        int x = arg;
    }

    // <init>
    {
        int x = 3;
    }

    // <clinit>
    static {
        int x = 2;
    }

    // Test11InitAndClinit
    // refer : https://hllvm-group.iteye.com/group/topic/35224
    public static void main(String[] args) {

        int x = 6;

    }

}

 

這樣就有了 javap 裏面看到的 這樣的結果, 也就是 題主所問的東西 

// <init> 
public com.hx.test11.Test11InitAndClinit();
public com.hx.test11.Test11InitAndClinit(int);

// <clinit> 
static {};

 

 

2. 關於 "<init>", "<clinit>" 的構造

另外對於這個問題, 還有一些 擴展的地方 :  {int x = 3; } 是怎麼被merge到 多個構造方法裏面的呢?, 多個 static {}, 怎麼 merge 到一個 <clinit> 的呢 ?

我們定位到 Gen. genClass 方法上面看一下 

normalizeDefs 之前, 我們發現 defs 列表, 和我們代碼結構是一致的 

 

normalizeDefs 之後, 就變成了 兩個 <init> 加一個 <clinit> 加一個 main 方法了, 這是怎麼回事呢 ?

normalizeDefs 到底做了什麼呢 ? 

 

Gen.normalizeDefs

    /** Distribute member initializer code into constructors and <clinit>
     *  method.
     *  @param defs         The list of class member declarations.
     *  @param c            The enclosing class.
     */
    List<JCTree> normalizeDefs(List<JCTree> defs, ClassSymbol c) {
        ListBuffer<JCStatement> initCode = new ListBuffer<JCStatement>();
        ListBuffer<JCStatement> clinitCode = new ListBuffer<JCStatement>();
        ListBuffer<JCTree> methodDefs = new ListBuffer<JCTree>();
        // Sort definitions into three listbuffers:
        //  - initCode for instance initializers
        //  - clinitCode for class initializers
        //  - methodDefs for method definitions
        for (List<JCTree> l = defs; l.nonEmpty(); l = l.tail) {
            JCTree def = l.head;
            switch (def.getTag()) {
            case JCTree.BLOCK:
                JCBlock block = (JCBlock)def;
                if ((block.flags & STATIC) != 0)
                    clinitCode.append(block);
                else
                    initCode.append(block);
                break;
            case JCTree.METHODDEF:
                methodDefs.append(def);
                break;
            case JCTree.VARDEF:
                JCVariableDecl vdef = (JCVariableDecl) def;
                VarSymbol sym = vdef.sym;
                checkDimension(vdef.pos(), sym.type);
                if (vdef.init != null) {
                    if ((sym.flags() & STATIC) == 0) {
                        // Always initialize instance variables.
                        JCStatement init = make.at(vdef.pos()).
                            Assignment(sym, vdef.init);
                        initCode.append(init);
                        if (endPositions != null) {
                            Integer endPos = endPositions.remove(vdef);
                            if (endPos != null) endPositions.put(init, endPos);
                        }
                    } else if (sym.getConstValue() == null) {
                        // Initialize class (static) variables only if
                        // they are not compile-time constants.
                        JCStatement init = make.at(vdef.pos).
                            Assignment(sym, vdef.init);
                        clinitCode.append(init);
                        if (endPositions != null) {
                            Integer endPos = endPositions.remove(vdef);
                            if (endPos != null) endPositions.put(init, endPos);
                        }
                    } else {
                        checkStringConstant(vdef.init.pos(), sym.getConstValue());
                    }
                }
                break;
            default:
                Assert.error();
            }
        }
        // Insert any instance initializers into all constructors.
        if (initCode.length() != 0) {
            List<JCStatement> inits = initCode.toList();
            for (JCTree t : methodDefs) {
                normalizeMethod((JCMethodDecl)t, inits);
            }
        }
        // If there are class initializers, create a <clinit> method
        // that contains them as its body.
        if (clinitCode.length() != 0) {
            MethodSymbol clinit = new MethodSymbol(
                STATIC, names.clinit,
                new MethodType(
                    List.<Type>nil(), syms.voidType,
                    List.<Type>nil(), syms.methodClass),
                c);
            c.members().enter(clinit);
            List<JCStatement> clinitStats = clinitCode.toList();
            JCBlock block = make.at(clinitStats.head.pos()).Block(0, clinitStats);
            block.endpos = TreeInfo.endPos(clinitStats.last());
            methodDefs.append(make.MethodDef(clinit, block));
        }
        // Return all method definitions.
        return methodDefs.toList();
    }

可以看到, 這裏是 將 instance variable initializer 和 instance initializer 放到了 initCode, 將 class variable initializer 和 static initializer 放到了 clinitCode 裏面 

其他的方法(包括構造方法), 放到 methodDefs 裏面 

然後針對構造方法, 插入 initCode 的相關代碼 

以及 構造 <clinit> 方法(class variable initializer + static initializer), 加入 methodDefs 

 

 

構造方法 結合 initCode 部分處理如下 

    /** Insert instance initializer code into initial constructor.
     *  @param md        The tree potentially representing a
     *                   constructor's definition.
     *  @param initCode  The list of instance initializer statements.
     */
    void normalizeMethod(JCMethodDecl md, List<JCStatement> initCode) {
        if (md.name == names.init && TreeInfo.isInitialConstructor(md)) {
            // We are seeing a constructor that does not call another
            // constructor of the same class.
            List<JCStatement> stats = md.body.stats;
            ListBuffer<JCStatement> newstats = new ListBuffer<JCStatement>();

            if (stats.nonEmpty()) {
                // Copy initializers of synthetic variables generated in
                // the translation of inner classes.
                while (TreeInfo.isSyntheticInit(stats.head)) {
                    newstats.append(stats.head);
                    stats = stats.tail;
                }
                // Copy superclass constructor call
                newstats.append(stats.head);
                stats = stats.tail;
                // Copy remaining synthetic initializers.
                while (stats.nonEmpty() &&
                       TreeInfo.isSyntheticInit(stats.head)) {
                    newstats.append(stats.head);
                    stats = stats.tail;
                }
                // Now insert the initializer code.
                newstats.appendList(initCode);
                // And copy all remaining statements.
                while (stats.nonEmpty()) {
                    newstats.append(stats.head);
                    stats = stats.tail;
                }
            }
            md.body.stats = newstats.toList();
            if (md.body.endpos == Position.NOPOS)
                md.body.endpos = TreeInfo.endPos(md.body.stats.last());
        }
    }

可以看出 處理之後的構造方法 大致分爲以下幾個批次, 並且順序如下 : 調用構造方法之前生成的代碼, 調用構造方法, 調用構造方法之後生成的代碼, instance variable initializer + instance initializer, 用戶自定義的構造方法代碼 

 

相關引用如下 

關於 instance variable initializer 和 class variable initializer : https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.3.2

關於 instance initializer 和 class initializer : https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.6 & https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.

 

 

3. 自己實現 javap? 

https://hllvm-group.iteye.com/group/topic/35386

呵呵呵, 自己實現 javap 似乎是沒有什麼太大的難度, 因爲 javac 已經將相關數據分析好了, 放到了 class 裏面, 只需要按照規範讀就行
h還是像 R大 說的, 直接參考 jdk 的 javap 就行, 

可以參考 jvms4.7 : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7

以及 javap 解析出來的對象, 呵呵呵 何必自己寫呢, 用現有的大佬寫好的東西 不好麼 (若是之前, 可能會去動手弄一下, 但是隨着空閒時間越來越有限, 呵呵 就懶得去動手了) 

 

不過這裏引申出了一個 討論, 關於class文件常量池 

----------------------------------------------------------------------
chenjingbo 2013-01-14 說道 : 
借這個寶地請教一下問題吧.我發現如果我的代碼是, 
Java代碼  收藏代碼
int a = 2000  ;  
在javap 查看以後,發現javac並不會在常量池中生成一個2000的Integer類型的常量.. 
但是, 
Java代碼  收藏代碼
final int a = 2000  ;  
這樣子就會生成一個2000的Integer類型的常量. 
我的問題是,既然前面那種情況是用 sipush的指令直接賦值2000,那麼,爲什麼在final的情況下不也直接如此賦值呢.這樣的話,常量池類型就可以減少4個(CONSTANT_Integer	CONSTANT_Float CONSTANT_Long	CONSTANT_Double) 

	
RednaxelaFX 2013-01-14 說道 : 
您沒把代碼寫完整。實際上您的例子是類似這樣的吧: 
Java代碼  收藏代碼
public class Foo {  
  int a = 2000;  
}  
Java代碼  收藏代碼
public class Foo {  
  final int a = 2000;  
}  
您可以試試把例子改爲在局部作用域裏賦值,就會發現加上final不會生成值爲2000的CONSTANT_Integer。 
Java裏類上的常量(static final的、原始類型或String的、有常量初始值的)的元數據必須記錄在Class文件裏;成員變量如果是final的、原始類型或String的、有常量初始值的,則這種常量信息也要被記錄在Class文件裏。這樣的話就需要在常量池記錄下常量值,然後在字段元數據裏記錄下ConstantValue屬性,讓它引用那個常量值。 
而且您說的那四中常量池類型即便在其它場景也是必須的。例如說大於2字節的int常量就只能放常量池裏用ldc指令來加載。
----------------------------------------------------------------------

首先我們先看一下 這個問題, 然後 在這個問題的基礎上面 再擴展一下 

 

測試代碼如下 

/**
 * Test12FinalVarInit
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2019/11/16 18:33
 */
public class Test12FinalVarInit {

    final int x = 200;

    // Test12VarInit
    public static void main(String[] args) {

        System.out.print(" end .. ");

    }

}

 

在 "final int x = 200;" 調試如下 

往常量池裏面添加 這個常量 200 的時候, 是在 往class裏面寫出字段 x 的時候, 發現 x 是常量, 然後寫出了 x 對應的值 放到常量池 

 

那麼什麼情況下 變量是 constantsValue 呢 ? 

如果變量是 final 變量, 並且 初始化值 不是 new 創建的實例[final x = new Integer(200); ]

 

那麼擴展一下 classfile 常量池 裏面的內容有哪些呢 ? 

查看一下 Pool.put 發現調用的地方主要是集中在兩個類 ClassWriter 和 Items 

ClassWriter 裏面的主要的業務, 可以從 writeClassFile 作爲入口查看, 這裏面主要是採集了字節碼裏面所需要的常量, 可以參照 字節碼文件規範 : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7

Items 裏面使用了常量池的地方主要是包含了三處地方 : MemberItem, StaticItem, ImmediateItem, 這三個 Items 主要是在遍歷方法, 爲方法生成字節碼的時候使用, 生成相關字節碼對應的字節碼[Code屬性] 

    MemberItem 主要是加載使用到的 成員變量, 方法 加載到常量池裏面 

    StaticItem 主要是加載使用到的 靜態變量, 靜態方法 加載到常量池裏面 

    ImmediateItem 主要是支持 ldc 指令的相關操作數, 將相關操作數存放到 常量池裏面 

 

當然有一些細節沒有提及, 就好比我們這裏上面的例子 "final int x = 200;", 200 進入了常量池, 細節層面的東西, 還是等需要的時候再去看吧, 可以在 ClassWriter 和 Items 裏面去搜索 "pool.put" 

 

 

4. 屬性表中屬性的區別難道是用字符串匹配?

https://hllvm-group.iteye.com/group/topic/35496

問題本身 好像是沒有什麼太大的爭議, 規範如此, 爲什麼這麼設計, R大 似乎也做了一些介紹 

呵呵, 我比較感興趣的事 作者定位到了 兩個字段對應在字節碼中的數據, 是怎麼做的呢 ? 

我比較傾向於直接使用 javap 來讀取吧, 直接拿到 解析之後的 "ClassFile" 對象 

 

測試代碼如下 

/**
 * Test13FieldAttribute
 *
 * @author Jerry.X.He <[email protected]>
 * @version 1.0
 * @date 2019/11/16 19:12
 */
public class Test13FieldAttribute {

    final float b = 1.2f;
    final int c = 1;

}

 

讀取class文件, 解析之後的 "ClassFile" 如下 

 

javap 反編譯如下 

G:\tmp\javac>javap -v Test13FieldAttribute.class
Classfile /G:/tmp/javac/Test13FieldAttribute.class
  Last modified 2019-11-16; size 341 bytes
  MD5 checksum ebdf79d6791914e583f9856778fcb932
  Compiled from "Test13FieldAttribute.java"
public class com.hx.test11.Test13FieldAttribute
  SourceFile: "Test13FieldAttribute.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#19         //  java/lang/Object."<init>":()V
   #2 = Float              1.2f
   #3 = Fieldref           #5.#20         //  com/hx/test11/Test13FieldAttribute.b:F
   #4 = Fieldref           #5.#21         //  com/hx/test11/Test13FieldAttribute.c:I
   #5 = Class              #22            //  com/hx/test11/Test13FieldAttribute
   #6 = Class              #23            //  java/lang/Object
   #7 = Utf8               b
   #8 = Utf8               F
   #9 = Utf8               ConstantValue
  #10 = Utf8               c
  #11 = Utf8               I
  #12 = Integer            1
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               SourceFile
  #18 = Utf8               Test13FieldAttribute.java
  #19 = NameAndType        #13:#14        //  "<init>":()V
  #20 = NameAndType        #7:#8          //  b:F
  #21 = NameAndType        #10:#11        //  c:I
  #22 = Utf8               com/hx/test11/Test13FieldAttribute
  #23 = Utf8               java/lang/Object
{
  final float b;
    flags: ACC_FINAL
    ConstantValue: float 1.2f

  final int c;
    flags: ACC_FINAL
    ConstantValue: int 1

  public com.hx.test11.Test13FieldAttribute();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // float 1.2f
         7: putfield      #3                  // Field b:F
        10: aload_0
        11: iconst_1
        12: putfield      #4                  // Field c:I
        15: return
      LineNumberTable:
        line 10: 0
        line 12: 4
        line 13: 10
}

 

兩個變量的輸入採集如下 

varName		accessFlags 	nameIdx		descIdx		attrCount	attrNameIdx		attrLength		constantValueIdx
b		0010 		0007 		0008 		0001		0009			0000 0002		0002
c		0010 		000A 		000B 		0001		0009			0000 0002		000C

採集的方法, 就是調試咯, 解析 字段的時候, 調試查詢對應的偏移區間, 進而進行查詢 

 

相關引用如下 

classFile規範 : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4

字段信息的存儲 :  https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.5

屬性信息的存儲 : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7

屬性ConstantValue : https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.2

 

 

完 

 

 

引用 

所有的引用已經在文檔中已經提及到了 

 

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