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

相关文章

使用Redis快速实现共享Session登录的详细步骤

《使用Redis快速实现共享Session登录的详细步骤》在Web开发中,Session通常用于存储用户的会话信息,允许用户在多个页面之间保持登录状态,Redis是一个开源的高性能键值数据库,广泛用于... 目录前言实现原理:步骤:使用Redis实现共享Session登录1. 引入Redis依赖2. 配置R

SpringBoot实现RSA+AES自动接口解密的实战指南

《SpringBoot实现RSA+AES自动接口解密的实战指南》在当今数据泄露频发的网络环境中,接口安全已成为开发者不可忽视的核心议题,RSA+AES混合加密方案因其安全性高、性能优越而被广泛采用,本... 目录一、项目依赖与环境准备1.1 Maven依赖配置1.2 密钥生成与配置二、加密工具类实现2.1

在Java中实现线程之间的数据共享的几种方式总结

《在Java中实现线程之间的数据共享的几种方式总结》在Java中实现线程间数据共享是并发编程的核心需求,但需要谨慎处理同步问题以避免竞态条件,本文通过代码示例给大家介绍了几种主要实现方式及其最佳实践,... 目录1. 共享变量与同步机制2. 轻量级通信机制3. 线程安全容器4. 线程局部变量(ThreadL

Django HTTPResponse响应体中返回openpyxl生成的文件过程

《DjangoHTTPResponse响应体中返回openpyxl生成的文件过程》Django返回文件流时需通过Content-Disposition头指定编码后的文件名,使用openpyxl的sa... 目录Django返回文件流时使用指定文件名Django HTTPResponse响应体中返回openp

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

破茧 JDBC:MyBatis 在 Spring Boot 中的轻量实践指南

《破茧JDBC:MyBatis在SpringBoot中的轻量实践指南》MyBatis是持久层框架,简化JDBC开发,通过接口+XML/注解实现数据访问,动态代理生成实现类,支持增删改查及参数... 目录一、什么是 MyBATis二、 MyBatis 入门2.1、创建项目2.2、配置数据库连接字符串2.3、入