本文是对«深入理解Java虚拟机»第六章第七章以及其他博客的总结, 权做笔记~
Class
文件是一组以 8
位字节为基础单位的二进制流, 当遇到需要占据 8
位字节以上空间的数据项时, 则会按照高位在前的方式分割成若干个 8
位字节进行存储
Class
文件格式采用一种类似于 C
语言结构体的伪结构来存储数据, 这种伪结构中只有两种数据类型: 无符号数和表; 无符号数是基本数据类型, 以 u1
, u2
, u4
, u8
来分别代表 1
个字节, 2
个字节, 4
个字节和 8
个字节的无符号数, 无符号数可以用来描述数字, 索引引用, 数量值或者按照 UTF-8
编码构成字符串值; 表由多个无符号数或者其他表作为数据项构成的复合数据类型, 以 _info
结尾
由于 Class
文件中没有分隔符, 所以哪个字节代表什么含义, 长度是多少, 先后顺序如何, 都被严格限定且不允许改变
Class
文件格式见下表
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
魔数: 每个 Class
文件头 4
个字节称为魔数, 唯一作用是确定该文件是否是一个能被虚拟机接受的 Class
文件
版本号: 紧接着魔数的四个字节存储的是 Class
文件的版本号: 第 5
和第 6
个字节是次版本号( Minor Version
), 第 7
和第 8
个字节是主版本号( Major Version
); 虚拟机必须拒绝执行超过其版本号的 Class
文件
常量池: 紧接着主版本号之后的是常量池入口, 是一个表类型数据项; Class
文件中只有常量池的容量计数是从 1
开始的; 常量池中主要存放两大类常量: 字面量和符号引用
final
访问标志: 常量池结束之后, 紧接着的两个字节代表访问标志; 用于表示一些类或者接口的层次信息
类索引, 父类索引和接口索引集合: 用于确定类的继承关系; 除了 java.lang.Object
之外, 所有的 Java
类都有父类, 因此除了 java.lang.Object
外, 所有 Java
类的父类索引都不为 0
字段表: 描述接口或者类中声明的变量; 字段包括类级变量以及实例级变量, 但不包括方法内部声明的局部变量
方法表: 包括访问标志, 名称索引, 描述符索引, 属性表集合; 如果父类方法在子类中没有被重写, 方法表集合中就不会出现来自父类的方法信息, 但是, 同样的, 有可能会出现由编译器自动添加的方法, 最典型的便是类构造器 <clinit>
方法和实例构造器 <init>
方法
属性表: Java
虚拟机运行时会忽略掉它不认识的属性
Java
程序中如果定义了超过 64KB
英文字符的变量或方法名, 将无法编译: 由于 Class
文件中方法, 字段等都需要引用 CONSTANT_Utf8_info
型常量来描述名称, 所以 CONSTANT_Utf8_info
型常量的最大长度也就是 Java
中方法, 字段名的最大长度; u2
类型能表达的最大值是 65535
, 所以 Java
程序中如果定义了超过 64KB
英文字符的变量或方法名, 将无法编译
类从被加载到虚拟机内存中开始, 到卸载出内存为止, 其生命周期如下图:
其中, 加载, 验证, 准备, 初始化和卸载这五个阶段的顺序是确定的, 解析阶段则不一定: 在某些情况下, 可以在初始化阶段之后再 开始 (注意, 是开始, 而不是 进行 或者 完成 )
遇到以下五种情况必须立即对类进行 初始化 (而加载, 验证, 准备自然要在这之前完成):
new
, getstatic
, putstatic
或 invokestatic
这 4
条字节码指令时, 如果类没有初始化, 需要出发初始化; 生成这 4
条指令的常见 Java
代码场景是: 使用 new
关键字实例化对象的时候, 读取或设置一个类的静态字段的时候(注意: 被 final
修饰, 已在编译期把结果放入常量池的静态字段除外(由此可见, 添加 final
修饰可以避免不必要的类加载)), 以及调用一个类的静态方法的时候 java.lang.reflect
包的方法对类进行反射调用的时候 main()
所在的类), 虚拟机会先初始化这个主类 JDK1.7
的动态语言支持(比如: Groovy
等)时, 如果一个 java.lang.invoke.MethodHandle
实例最后的解析结果 REF_getStatic
, REF_putStatic
, REF_invokeStatic
的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需先触发其初始化 考虑下列代码:
public class Practice { static { System.out.println("Practice static code block"); } { System.out.println("Practice code block"); } public Practice() { System.out.println("Practice constructor"); } }
当 new
一个 Practice
对象时, 输出如下; 说明三者的执行顺序是: 静态代码块 –> 代码块 –> 构造函数
Practice static code block Practice code block Practice constructor
关于三者的执行时机与作用:
main()
前面所说的触发初始化的五个场景对应叫做主动引用
public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); // 不会初始化子类~ } } class SuperClass { static { System.out.println("SuperClass init"); } public static int value = 123; } class SubClass extends SuperClass{ static { System.out.println("Child init"); } }
注: 后文为节省篇幅, 将重用这里的 SuperClass
和 SubClass
, 而不再重写~
Object
的子类; Java
中对数组的访问比 C/C++
更安全, 是因为这个类封装了数组元素的访问方法, 而 C/C++
直接翻译为数组指针的移动 public class NotInitialization { public static void main(String[] args) { SuperClass[] sca = new SuperClass[0]; } }
Hello World
常量存储到了 NotInitialization
类的常量池中了, 以后 Initialization
对常量 ConstClass.HELLOWORLD
的引用都被转化为 NotInitialization
类对自身常量池的引用了~ public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.CONST); } } class ConstClass { static { System.out.println("ConstClass init"); } public static final String HELLOWORLD="Hello World"; // 注意这里不能从static来理解, 因为即使有static, 但是没有final的话仍然会触发ConstClass的初始化~ }
结合前面的图咯~
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class
对象(该 Class
对象并没有明确规定是在 Java
堆中, 对于 HotSpot
虚拟机而言, Class
对象比较特殊, 它虽然是对象, 但是存在于方法区中), 作为方法区这个类的各种数据的访问入口
0xCAFEBABE
final final
对类的方法体进行校验分析, 保证被校验的类在运行时不会危害虚拟机
正式为类变量分配内存并为其设置初始值
将常量池中的符号引用替换为直接引用
真正开始执行 Java
程序代码(字节码)或者说初始化阶段是执行类构造器 <clinit>()
方法的过程
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块( static
代码块)中的语句合并产生的; 静态语句块只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问, 如下代码 class Temp { static { value = 2; // System.out.println(value); // 不能访问 } static int value = 1; }
<clinit>()
方法与构造函数( <init>()
)不同, 虚拟机会保证子类的 <clinit>()
方法执行之前, 父类的 <clinit>()
方法已经执行完毕; 因此在虚拟机中第一个被执行的 <clinit>()
方法的类肯定是 java.lang.Object
由于父类的 <clinit>()
方法先于子类的执行, 也就意味着父类中的静态语句块要先于子类的变量赋值操作; 注意如下代码;
class SuperClass { public static int A = 1; static { A = 2; } } class SubClass extends SuperClass{ public static int B = A; // 注意这里的B应该为2, 而不是1 }
<clinit>()
方法对于类或接口来说不是必需的, 如果一个类没有静态语句块, 也没有对变量的赋值操作, 那么编译器可以不为这个类生成 <clinit>()
方法
接口中不能有静态语句块, 但是可以有变量的初始化赋值操作, 因此接口与类一样也会生成 <clinit>()
方法; 但接口与类不同的是, 执行接口的 <clinit>()
方法不需要先执行父接口的 <clinit>()
方法, 只有当父接口中定义的变量使用时, 父接口才会初始化; 另外, 接口的实现类在初始化时也一样不会执行接口的 <clinit>()
方法
虚拟机会保证一个类的 <clinit>()
方法在多线程环境中被正确的加锁, 同步; 只能有一个线程去执行 <clinit>()
任意一个类, 都是由加载它的类加载器和这个类本身一同确立其在 Java
虚拟机中的唯一性; 更通俗一些是: 比较两个类是否 相等 , 只有在这两个类是由同一个类加载器加载的前提下才有意义, 否则, 即使这两个类来自同一个 Class
文件, 被同一个虚拟机加载, 只要加载它们的类加载器不同, 那这两个类就必定不相等(这里所谓的相等, 包括代表 Class
对象的 equals()
方法, isAssignableFrom()
方法, isInstance()
方法的返回结果, 以及使用 instanceof
关键字做对象所属关系判断等情况); 如下代码:
import java.io.IOException; import java.io.InputStream; public class ClassLoaderTest { public static void main(String[] args) throws Exception { ClassLoader loader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream inputStream = getClass().getResourceAsStream(fileName); if (inputStream == null) { return super.loadClass(name); } byte[] bytes = new byte[inputStream.available()]; inputStream.read(bytes); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { } return null; } }; Object obj = loader.loadClass("Practice").newInstance(); System.out.println(obj.getClass()); System.out.println(obj instanceof Practice); // 及时是Practice实例, 但是这里仍然返回false } }
启动类加载器( Bootstrap ClassLoader
): C++
实现, 虚拟机自身一部分
扩展类加载器( Extension ClassLoader
)
应用程序类加载器( Application ClassLoader
): 如果没有定义自己的类加载器, 则为程序中默认的类加载器
如下图; 双亲委派模型要求除了顶层的启动类加载器外, 其余的类加载器都应当有自己的父类加载器;
如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此; 因此所有的加载请求最终都应该传送到顶层的启动类加载器中, 只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时, 子加载器才会尝试自己去加载