前言
呵呵 最近看到了一系列跟 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.7
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
完
引用
所有的引用已經在文檔中已經提及到了