01 一些关于java编译器的问题(init, clinit的生成, 自己实现javap?)

2024-05-28 15:32

本文主要是介绍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 <970655147@qq.com>* @version 1.0* @date 2019/11/16 17:00*/
public class Test11InitAndClinit {// xprivate 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/35224public 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 definitionsfor (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);elseinitCode.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 callnewstats.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 <970655147@qq.com>* @version 1.0* @date 2019/11/16 18:33*/
public class Test12FinalVarInit {final int x = 200;// Test12VarInitpublic 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 <970655147@qq.com>* @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.classLast modified 2019-11-16; size 341 bytesMD5 checksum ebdf79d6791914e583f9856778fcb932Compiled from "Test13FieldAttribute.java"
public class com.hx.test11.Test13FieldAttributeSourceFile: "Test13FieldAttribute.java"minor version: 0major version: 51flags: 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_FINALConstantValue: float 1.2ffinal int c;flags: ACC_FINALConstantValue: int 1public com.hx.test11.Test13FieldAttribute();flags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: ldc           #2                  // float 1.2f7: putfield      #3                  // Field b:F10: aload_011: iconst_112: putfield      #4                  // Field c:I15: returnLineNumberTable:line 10: 0line 12: 4line 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

 

 

完 

 

 

引用 

所有的引用已经在文档中已经提及到了 

 

这篇关于01 一些关于java编译器的问题(init, clinit的生成, 自己实现javap?)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1010960

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2