类的结构
CA FE BA BE(魔数) | Minor Version(次版本号) | Major Version(主版本号) |
---|---|---|
Constant Pool Count(常量池数量) | Constant Pool(常量池) | |
Access Flags(访问标记) | This Class(当前类) | Super Class(父类) |
Interface Count(接口数量) | Interfaces | |
Field Count(字段集合) | Fields | |
Method Count(方法集合) | Methods | |
Attribute Count(属性集合) | Attributes |
魔数
Class 文件的头4个字节被称为魔数,用来标识此文件是否是一个能被虚拟机接收的Class文件
Class 版本号
魔数后面的4个字节表示Class文件的版本号,第5、6位是次版本号,第7、8位是主版本号,用来表示此Class文件是被哪个版本的java编译器编辑的
注意:JVM是向下兼容的,高版本的JVM能运行低版本的字节码
常量池
版本号之后的是常量池,常量池的数量是 constant_pool_count-1(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)
主要存放着字面量和符号引用两种常量,包括:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
访问标志
访问标志占用两个字节,用来表示类或者接口层次的访问信息,如 public、abstract、final 等当前类&父类&接口索引集合
用于描述当前类、其父类和其实现的所有接口的全限定名(全类名,非限定名即短类名)字段表集合
用于描述当前类或接口中的全局变量,不包括方法内部的局部变量方法表集合
和字段表集合类似,用来描述类中方法的信息属性表集合
字段表和方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息(如字段或者方法上面的注解,方法抛出的异常,code属性等)类加载过程
编译好的 class 文件需要加载到JVM中才能够被运行和使用
JVM加载class文件主要分为三步:加载、连接、初始化
其中连接又分为三步:验证、准备、解析
注意:数组由JVM直接创建,不需要被加载;用户可以通过重写loadClass()自定义类加载器加载
主要完成三件事情
- 通过全类名获取定义此类的二进制字节流
- 将字节流中的静态存储结构转换为方法区中的运行时数据结构(内部采用 C++ 的instanceKlass 来描述 java 类)
- 在内存中生成一个代表该类 class 的类对象(java_mirror,也叫 java镜像类),作为方法区这些数据的入口(java_mirror和instanceKlass互相保存的对方的地址,堆中的实例对象的对象头中则会保存 java_mirror 的地址,方便实例对象通过类对象找到方法区中的 instanceKlass,以此获取类的各种信息(常量池、方法、类加载器、静态常量等))
注意:若这个类的父类还没有被加载,则会先加载父类;加载和链接可能是交替执行的
链接
验证
验证class文件的内容是否符合JVM规范,并进行一些安全性检查和初始化零值等操作
准备
为 static 变量分配空间,设置默认值(初始化零值)
- static变量 在jdk1.7以前存储在instanceKlass中(方法区),1.7及之后存在了java_mirror类对象中(堆)
- 如果 static 是 final 的基本数据类型或是字符串常量,在编译期阶段值就确定了,那么赋值操作在准备阶段完成
- 如果 static 是 final 的引用数据类型,那么赋值操作在初始化阶段完成
解析
将方法区运行时常量池中的符号引用解析成直接引用
符号引用:符号引用就是用一组符号描述目标,通过解析符号引用可以定位到目标方法或类
直接引用:直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄
解析阶段其实是JVM将运行时常量池中的符号引用转换成直接引用的过程,从而能够获取想要的字段、类或方法在内存中的指针或者偏移量初始化
初始化阶段会执行<cinit>()
方法(编译器自动生成的方法),是类加载的最后一部;这个方法是带锁线程安全的,在多线程环境下进行类初始化可能会引起阻塞问题
只有5种情况,必须对类进行初始化:使用
初始化完成后就可以使用静态变量、创建对象等操作卸载
卸载即将堆空间中的类对象进行GC回收,需要满足三个要求:
- 该类的所有对象都收回
- 该类在其它地方没有被引用
-
类加载器
所有的类都需要通过类加载器进行加载,作用只有一个,将class文件内容加载到运行时数据区
类加载器种类
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
BootstrapClassLoader:启动类加载器,由C++实现,负责加载JAVA_HOME/lib 目录下的jar包或者被-Xbootclasspath参数指定路径的所有类
- ExtensionClassLoader:扩展类加载器,主要负责加载JRE_HOME/lib/ext 目录下的jar包和类,或者被java.ext.dirs 系统变量所指定的路径下的jar包
- AppClassLoader:应用程序类加载器,面向用户的加载器,负责加载当前应用 classpath 下的jar包或类
ExtensionClassLoader 是 AppClassLoader 的父类加载器,BootstrapClassLoader 是 ExtensionClassLoader 的父类加载器(在代码中 ExtensionClassLoader 的父类加载器为 null)
双亲委派模型
每个类都有一个对应的加载器,ClassLoader 在工作时会默认使用 双亲委派机制
工作流程
在类加载的时候,首先会判断当前类是否被加载,若被加载则会直接返回;若没有被加载,则会首先尝试将加载请求委派给父类加载器的 loadClass();正常来说所有的加载请求最终都会传递到BootstrapClassLoader中,当父类加载器无法处理时,才会由自己的类加载器处理
作用
- 可以避免类的重复加载,提高运行效率
保证了Java的核心API不被篡改(若我们写了一个和核心API同名的类,如java.lang.Object,就会导致系统出现不同的Object类)
如何不用双亲委派模型
自定义加载器,需要继承 ClassLoader 类
如果不想打破双亲委派模型,重写 findClass() 方法即可,当所有父类加载器都无法加载时,则会使用自定义加载器
- 如果想打破双亲委派模型直接使用自定义加载器,则重写 loadClass() 即可
注意:除了BootstrapClassLoader以外,其它类加载器均继承自 java.lang.ClassLoader