这是 Java 基础篇 (JVM) 的第一篇文章, 本来想先说说 Java 类加载机制的, 后来想想, JVM 的作用是加载编译器编译好的字节码, 并解释成机器码, 那么首先应该了解字节码, 然后再谈加载字节码的类加载机制似乎会好些, 所以这篇改成详解字节码.
由于 Java 纯面向对象的特性, 字节码只要能表示一个类的信息, 就可以表示整个 Java 程序了, JVM 只要能加载一个类的信息, 就能加载整个程序了. 所以, 不管是字节码, 还是 JVM 加载机制, 关注点都是在类. 我关注的点主要在于:
1. 由于字节码不是一次性全部加载进入内存, 那么 JVM 是如何知道自己要加载的类信息在. class 文件的哪个位置的?
2. 字节码是如何表示类信息的?
3. 字节码会进行程序的优化吗?
第一个问题很简单, 因为哪怕一个源文件有很多个类(只有一个 public 类), 编译器也会为其中每个类都生成一个. class 文件, JVM 加载时按照需要加载的类名称加载即可.
要解决后面的问题, 首先我们来看字节码的组成(Mac 下用 Hex Fiend 打开).
对这样一段代码:
- package com.test.main1;
- public class ByteCodeTest {
- int num1 = 1;
- int num2 = 2;
- public int getAdd() {
- return num1 + num2;
- }
- }
- class Extend extends ByteCodeTest {
- public int getSubstract() {
- return num1 - num2;
- }
- }
我们来分析其中的 Extend 类.
用 Hex Fiend 打开编译后的. class 文件是这样的(16 进制代码):
由于 class 文件没有分隔符, 所以每个位置代表什么, 各个部分的长度等格式是严格规定死的, 见下表:
其中 u1,u2,u4,u8 代表几个字节的无符号数, 在反编译出来的 16 进制文件中, 两个数字代表一个字节, 也就是 u1.
从头到尾一项一项地看:
(1)magic:u4, 魔数, 代表本文件是. class 文件..jpg 等也会有这种魔数, 正因为魔数存在, 即使将 *.jpg 改成 *.123, 也能照常打开.
(2)minor version,major version: 各 u2, 版本号, 向下兼容, 即高版本 JDK 可以使用低版本的. class 文件, 反之不行.
(3)constant_pool_count:u2, 常量池中常量的数量, 0019 代表有 24 个.
(4)接下来就是具体的常量, 共 constant_pool_count-1 个.
常量池通常存两种类型的数据:
字面量: 如字符串, final 修饰的常量等;
符号引用: 如类 / 接口的全限定名, 方法的名称和描述, 字段的名称和描述等.
根据反编译出来的数字, 首先查下表得到该常量的类型和长度, 接下来的与查得的长度相等的数字则表示该常量具体的值.
如 070002, 就表示该种类型为 CONSTANT_Class_info, 它的 tag 为 u1, 且接下来 u2 长度为 index 指向全限定名常量项的索引. 这个索引还要结合 javap -verbose 打开的 class 文件一起看, 这里清晰地列出了常量池中的内容和顺序:
在这里可以看到 0002 索引项的常量为: com/test/main1/Extend, 是类的全限定名. 如果是值是字符串, 那么需要根据该值转换成十进制并查 ASCII 码表得到具体的字符. 接下来的常量都照此分析:
- 01001563 6F6D2F74 6573742F 6D61696E 312F4578 74656E64:com/test/main1/Extend
- 070004:com/test/main1/ByteCodeTest
- 01001B63 6F6D2F74 6573742F 6D61696E 312F4279 7465436F 64655465 7374:com/test/main1/ByteCodeTest
- 0100063C 696E6974 3E:<init>
- 01000328 2956:()V
- 01000443 6F6465:Code
- 0A000300 09:com/test/main1/ByteCodeTest,"<init>":()V
- 0C000500 06:<init>,()V
- 01000F4C 696E654E 756D6265 72546162 6C65:LineNumberTable
- 0100124C 6F63616C 56617269 61626C65 5461626C 65:LocalVariableTable
- 01000474 686973:this
- 0100174C 636F6D2F 74657374 2F6D6169 6E312F45 7874656E 643B:Lcom/test/main1/Extend;
- 01000C67 65745375 62737472 616374:getSubstract
- 01000328 2949:()I
- 09000100 11:com/test/main1/Extend,num1:I
- 0C001200 13:num1,I
- 0100046E 756D31:num1
- 01000149:I
- 09000100 15:com/test/main1/Extend,num2:I
- 0C001600 13:num2,I
- 0100046E 756D32:num2
- 01000A53 6F757263 6546696C 65:SourceFile
- 01001142 79746543 6F646554 6573742E 6A617661:ByteCodeTest.java
至此, 常量池中的常量全部解析完毕.
(5)再接下来是 u2 的 access_flags:access_flags 访问标志的主要目的是标记该类是类还是接口, 如果是类, 访问权限是否为 public, 是否是 abstract, 是否被标志为 final 等, 见下表:
Flag_name | Value | Interpretation |
ACC_PUBLIC | 0x0001 | 表示访问权限为 public,可以从本包外访问 |
ACC_FINAL | 0x0010 | 表示由 final 修饰,不允许有子类 |
ACC_SUPER | 0x0020 | 较为特殊,表示动态绑定直接父类,见下面的解释 |
ACC_INTERFACE | 0x0200 | 表示接口,非类 |
ACC_ABSTRACT | 0x0400 | 表示抽象类,不能实例化 |
ACC_SYNTHETIC | 0x1000 | 表示由 synthetic 修饰,不在源代码中出现,见附录 < sup ztid="157" ow="20" oh="16">[2] |
ACC_ANNOTATION | 0x2000 | 表示是 annotation 类型 |
ACC_ENUM | 0x4000 | 表示是枚举类型 |
所以, 本类中的 access_flags 是 0020, 表示这个 Extend 类调用父类的方法时, 并非是编译时绑定, 而是在运行时搜索类层次, 找到最近的父类进行调用. 这样可以保证调用的结果是一定是调用最近的父类, 而不是编译时绑定的父类, 保证结果的正确性. 这个可以参见文章[1].
(6)this_class:u2 的类索引, 用于确定类的全限定名. 本类的 this_class 是 0001, 表示在常量池中 #1 索引, 是 com/test/main1/Extend
(7)super_class:u2 的父类索引, 用于确定直接父类的全限定名. 本类是 0003,#3 是 com/test/main1/ByteCodeTest
(8)interfaces_count:u2, 表示当前类实现的接口数量, 注意是直接实现的接口数量. 本类中是 0000, 表示没有实现接口.
(9)Interfaces: 表示接口的全限定名索引. 每个接口 u2, 共 interfaces_count 个. 本类为空.
(10)fields_count:u2, 表示类变量和实例变量总的个数. 本类中是 0000, 无.
(11)fields:fileds 的长度为 filed_info,filed_info 是一个复合结构, 组成如下:
- filed_info: {
- u2 access_flags;
- u2 name_index;
- u2 descriptor_index;
- u2 attributes_count;
- attribute_info attributes[attributes_count];
- }
由于本类无类变量和实例变量, 故本字段为空.
(12)methods_count:u2, 表示方法个数. 本类中是 0002, 表示有 2 个.
(13)methods:methods 的长度为一个 method_info 结构:
- method_info {
- u2 access_flags; 0000 ?
- u2 name_index; 0005 <init>
- u2 descriptor_index; 0006 ()V
u2 attributes_count; 0001 1 个
- attribute_info attributes[attributes_count]; 0007 Code
- }
其中 attribute_info 结构如下:
- attribute_info {
- u2 attribute_name_index; 0007 Code
- u1 attribute_length;
- u1 info[attribute_length];
- }
上面是通用的 attribute_info 的定义, 另外, JVM 里预定义了几种 attribute,Code 即是其中一种(注意, 如果使用的是 JVM 预定义的 attribute, 则 attribute_info 的结构就按照预定义的来), 其结构如下:
- Code_attribute { //Code_attribute 包含某个方法, 实例初始化方法, 类或接口初始化方法的 Java 虚拟机指令及相关辅助信息
- u2 attribute_name_index; 0007 Code
- u4 attribute_length; 0000002F 47
- u2 max_stack; 0001 1 // 用来给出当前方法的操作数栈在方法执行的任何时间点的最大深度
- u2 max_locals; 0001 1 // 用来给出分配在当前方法引用的局部变量表中的局部变量个数
- u4 code_length; 00000005 5 // 给出当前方法 code[]数组的字节数
- u1 code[code_length]; 2AB70008 B1 42,183,0,8,177
- // 给出了实现当前方法的 Java 虚拟机代码的实际字节内容 (这些数字代码实际对应一些 Java 虚拟机的指令)
- u2 exception_table_lentgh; 0000 0 // 异常的信息
- {
- u2 start_pc; // 这两项的值表明了异常处理器在 code[]中的有效范围, 即异常处理器 x 应满足: start_pc≤x≤end_pc
- u2 end_pc; //start_pc 必须在 code[]中取值, end_pc 要么在 code[]中取值, 要么等于 code_length 的值
- u2 handler_pc; // 表示一个异常处理器的起点
- u2 catch_type; // 表示当前异常处理器需要捕捉的异常类型. 为 0, 则都调用该异常处理器, 可用来实现 finally.
} exception_table[exception_table_lentgh]; 在本类中大括号里的结构为空
u2 attribute_count; 0002 2 表示该方法的其它附加属性, 本类有 1 个
- attribute_info attributes[attributes_count]; 000A,000B LineNumberTable,LocalVariableTable
- }
LineNumberTable 和 LocalVariableTable 又是两个预定义的 attribute, 其结构如下:
- LineNumberTable_attribute { // 被调试器用来确定源文件中由给定的行号所表示的内容, 对应于 Java 虚拟机 code[]数组的哪部分
- u2 attribute_name_index; 000A
- u4 attribute_length; 00000006
- u2 line_number_table_length; 0001
- { u2 start_pc; 0000
- u2 line_number; 000E // 该值必须与源文件中对应的行号相匹配
- } line_number_table[line_number_table_length];
- }
以及:
- LocalVariableTable_attribute {
- u2 attribute_name_index; 000B
- u4 attribute_length; 0000000C
- u2 local_variable_table_length; 0001
- { u2 start_pc; 0000
- u2 length; 0005
- u2 name_index; 000C
- u2 descriptor_index; 000D // 用来表示源程序中局部变量类型的字段描述符
- u2 index; 0000
- } local_variable_table[local_variable_table_length];
然后就是第二个方法, 具体略过.
(14)attributes_count:u2, 这里的 attribute 表示整个 class 文件的附加属性, 和前面方法的 attribute 结构相同. 本类中为 0001.
(15)attributes:class 文件附加属性, 本类中为 0017, 指向常量池 #17, 为 SourceFile,SourceFile 的结构如下:
- SourceFile_attribute {
- u2 attribute_name_index; 0017 SourceFile
- u4 attribute_length; 00000002 2
- u2 sourcefile_index; 0018 ByteCodeTest.java // 表示本 class 文件是由 ByteCodeTest.java 编译来的
- }
嗯, 字节码的内容大概就写这么多. 可以看到通篇文章基本都是在分析字节码文件的 16 进制代码, 所以可以这么说, 字节码的核心在于其 16 进制代码, 利用规范中的规则去解析这些代码, 可以得出关于这个类的全部信息, 包括:
1. 这个类的版本号;
2. 这个类的常量池大小, 以及常量池中的常量;
3. 这个类的访问权限;
4. 这个类的全限定名, 直接父类全限定名, 类的直接实现的接口信息;
5. 这个类的类变量和实例变量的信息;
6. 这个类的方法信息;
7. 其它的这个类的附加信息, 如来自哪个源文件等.
解析完字节码, 回头再来看开始提出的问题, 也就迎刃而解了. 由于字节码文件格式严格按照规定, 可以用来表示类的全部信息; 字节码只是用来表示类信息的, 不会进行程序的优化.
那么在编译期间, 编译器会对程序进行优化吗? 运行期间 JVM 会吗? 什么时候进行的, 按照什么原则呢? 这个留作以后再表.
最后, 值得注意的是, 字节码不仅是平台无关的(任何平台生成的字节码都可以在任何的 JRE 环境运行), 还是语言无关的, 不仅 Java 可以生成字节码, 其它语言如 Groovy,Jython,Scala 等也能生成字节码, 运行在 JRE 环境中.
参考文章
[1] https://blog.csdn.NET/xinaij/article/details/38872851
[2] synthetic 关键字不是人为添加的, 而是编译器基于程序逻辑自动添加的, 可以修饰方法, 也可以修饰类. 通常出现在有内部类, 且内部类访问权限为 private 的时候.
我们可以在外部类中调用内部类的 private 方法, 访问 private 属性. 但其实编译器对所有的类包括内部类, 都是当做顶级类来编译的, 这就是说一个顶级类可以访问另一个顶级类的私有方法, 显然有问题. 为了不出错, 编译器对内部类的私有属性都加上了 synthetic 修饰的 access 方法, 类似于 setter/getter 方法, 使得外部类可以访问内部类的私有属性. 私有方法也一样, 加了一个具有包访问权限的方法, 调用私有方法, 使得外部类可以调用私有方法.
当内部类的访问权限为 private 的话, 照理来说只能本类访问, 你是不可能在程序其它地方通过 OuterClass.InnerClass 来 new 一个内部类对象的, 但是我们经常这么做, 而且还没出错, 原因就是编译器帮我们合成了一个具有包访问权限的合成类(也就是具有包访问权限的构造器). 这个还不是很清楚, 但是大体的思路应该与私有属性和方法类似.
- https://blog.csdn.NET/zhang_yanye/article/details/50301511
- https://www.cnblogs.com/bethunebtj/p/7761596.html
[3] 之前看别人的文章我一直有个疑问, 他们这些知识是哪里来的? 现在慢慢搞明白了, 很多都是规范上截取的. 比如这篇, 我就参考了很多《Java 虚拟机规范》中的内容. 授人以鱼不如授人以渔, 感兴趣可以翻翻这篇.
来源: https://www.cnblogs.com/lilei94/p/9744331.html