本文主要讲解“Java类文件的基本结构是什么”,感兴趣的朋友不妨看看。本文介绍的方法简单、快速、实用。让边肖带你学习“Java类文件的基本结构是什么”!
00-
Class文件
什么是Class文件?
java之所以能够实现跨平台,是因为在其编译阶段,它不是直接将代码编译成平台相关的机器语言,而是先将java字节码编译成二进制形式,放入Class文件中,然后虚拟机加载Class文件来分析程序所需的内容。每个类将被编译成一个单独的类文件,内部类将作为一个独立的类来生成自己的类。
00-1010只需找到一个类文件,用崇高文本打开它,如下所示:
你是不是看起来很傻,但是java虚拟机规范给出了类文件的基本格式,就按照这个格式解析吧:
ClassFile{
u4magic
u2minor _ version
U2 main _ version;
u2constant _ pool _ count
CP _ info constant _ pool[constant _ pool _ count-1];
u2access _ flags
u2this _ class
u2super _ class
u2interfaces _ count
U2 interfaces[interfaces _ count];
u2fields _ count
field _ info field[field _ count];
u2methods _ count
method _ info methods[methods _ count];
u2attributes _ count
attribute _ info attributes[attributes _ count];
}ClassFile中的字段类型是u1、u2和u4。这是什么类型的?其实很简单,就是分别表示1字节、2字节、4字节。
前四个字节为:magic,用于唯一标识文件格式,一般称为幻数,使虚拟机能够识别加载的文件是否为类格式,类文件的幻数为cafebabe。不仅仅是类文件,基本上大多数文件都有神奇的数字来识别自己的格式。
下一部分主要包含类文件的一些信息,比如常量池、类访问标志、父类、接口信息、字段、方法等等。
基本结构
解析
lass="header-link octicon octicon-link">字段类型
上面说到ClassFile中的字段类型有u1、u2、u4,分别表示1个字节,2个字节和4个字节的无符号整数。java中short、int、long分别为2、4、8个字节的有符号整数,去掉符号位,刚好可以用来表示u1、u2、u4。
public class U1 { public static short read(InputStream inputStream) { byte[] bytes = new byte[1]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } short value = (short) (bytes[0] & 0xFF); return value; } } public class U2 { public static int read(InputStream inputStream) { byte[] bytes = new byte[2]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } int num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } } public class U4 { public static long read(InputStream inputStream) { byte[] bytes = new byte[4]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } long num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } }
常量池
定义好字段类型后,我们就可以读取class文件了,首先是读取魔数之类的基本信息,这部分很简单:
FileInputStream inputStream = new FileInputStream(file); ClassFile classFile = new ClassFile(); classFile.magic = U4.read(inputStream); classFile.minorVersion = U2.read(inputStream); classFile.majorVersion = U2.read(inputStream);
这部分只是热热身,接下来的大头在于常量池。解析常量池之前,我们先来解释一下常量池是什么。
常量池,顾名思义,存放常量的资源池,这里的常量指的是字面量和符号引用。字面量指的是一些字符串资源,而符号引用分为三类:类符号引用、方法符号引用和字段符号引用。通过将资源放在常量池中,其他项就可以直接定义成常量池中的索引了,避免了空间的浪费,不只是class文件,Android可执行文件dex也是同样如此,将字符串资源等放在DexData中,其他项通过索引定位资源。java虚拟机规范给出了常量池中每一项的格式:
cp_info { u1 tag; u1 info[]; }
由于格式太多,文章中只挑选一部分讲解:
这里首先读取常量池的大小,初始化常量池:
//解析常量池 int constant_pool_count = U2.read(inputStream); ConstantPool constantPool = new ConstantPool(constant_pool_count); constantPool.read(inputStream);
接下来再逐个读取每项内容,并存储到数组cpInfo中,这里需要注意的是,cpInfo[]下标从1开始,0无效,且真正的常量池大小为constant_pool_count-1。
public class ConstantPool { public int constant_pool_count; public ConstantInfo[] cpInfo; public ConstantPool(int count) { constant_pool_count = count; cpInfo = new ConstantInfo[constant_pool_count]; } public void read(InputStream inputStream) { for (int i = 1; i < constant_pool_count; i++) { short tag = U1.read(inputStream); ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag); constantInfo.read(inputStream); cpInfo[i] = constantInfo; if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) { i++; } } } }
我们先来看看CONSTANT_Utf8格式,这一项里面存放的是MUTF-8编码的字符串:
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
那么如何读取这一项呢?
public class ConstantUtf8 extends ConstantInfo { public String value; @Override public void read(InputStream inputStream) { int length = U2.read(inputStream); byte[] bytes = new byte[length]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } try { value = readUtf8(bytes); } catch (UTFDataFormatException e) { e.printStackTrace(); } } private String readUtf8(byte[] bytearr) throws UTFDataFormatException { //copy from java.io.DataInputStream.readUTF() } }
很简单,首先读取这一项的字节数组长度,接着调用readUtf8(),将字节数组转化为String字符串。
再来看看CONSTANT_Class这一项,这一项存储的是类或者接口的符号引用:
CONSTANT_Class_info { u1 tag; u2 name_index; }
注意这里的name_index并不是直接的字符串,而是指向常量池中cpInfo数组的name_index项,且cpInfo[name_index]一定是CONSTANT_Utf8格式。
public class ConstantClass extends ConstantInfo { public int nameIndex; @Override public void read(InputStream inputStream) { nameIndex = U2.read(inputStream); } }
常量池解析完毕后,就可以供后面的数据使用了,比方说ClassFile中的this_class指向的就是常量池中格式为CONSTANT_Class的某一项,那么我们就可以读取出类名:
int classIndex = U2.read(inputStream); ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex]; ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex]; classFile.className = className.value; System.out.print("classname:" + classFile.className + "\n");
字节码指令
解析常量池之后还需要接着解析一些类信息,如父类、接口类、字段等,但是相信大家最好奇的还是java指令的存储,大家都知道,我们平时写的java代码会被编译成java字节码,那么这些字节码到底存储在哪呢?别急,讲解指令之前,我们先来了解下ClassFile中的method_info,其格式如下:
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
method_info里主要是一些方法信息:如访问标志、方法名索引、方法描述符索引及属性数组。这里要强调的是属性数组,因为字节码指令就存储在这个属性数组里。属性有很多种,比如说异常表就是一个属性,而存储字节码指令的属性为CODE属性,看这名字也知道是用来存储代码的了。属性的通用格式为:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
根据attribute_name_index可以从常量池中拿到属性名,再根据属性名就可以判断属性种类了。
Code属性的具体格式为:
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
其中code数组里存储就是字节码指令,那么如何解析呢?每条指令在code[]中都是一个字节,我们平时javap命令反编译看到的指令其实是助记符,只是方便阅读字节码使用的,jvm有一张字节码与助记符的对照表,根据对照表,就可以将指令翻译为可读的助记符了。这里我也是在网上随便找了一个对照表,保存到本地txt文件中,并在使用时解析成HashMap。代码很简单,就不贴了,可以参考我代码中InstructionTable.java。
接下来我们就可以解析字节码了:
for (int j = 0; j < methodInfo.attributesCount; j++) { if (methodInfo.attributes[j] instanceof CodeAttribute) { CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j]; for (int m = 0; m < codeAttribute.codeLength; m++) { short code = codeAttribute.code[m]; System.out.print(InstructionTable.getInstruction(code) + "\n"); } } }
到此,相信大家对“Java class文件基本结构是怎样的”有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/154309.html