一,类文件结构
Class文件是一组以8字节为基础单位的二进制流。各个数据严格按照顺序紧凑地排列在Class文件中。
Class文件包含两种数据类型:无符号数和表
无符号数:u1,u2,u4,u8(分别代表1,2,4,8字节)
表:由多个无符号数或者其它表作为数据项构成的复合数据类型。所有表习惯性以“.info”结尾
class文件中的每个字节,长度,先后顺序都是被严格控制,不允许改变。
class文件结构:
魔数——class文件版本——常量池——访问标志——类索引,父类索引与接口索引集合——字段表集合——方法表集合——属性表集合(关于这部分可以直接通过javap反编译出来,笔者再此不作过多介绍)
二,类加载机制
类加载的生命周期:加载-验证-准备-解析-初始化-使用-卸载
为了支持java语言的动态绑定,除了加载-验证-准备-初始化-卸载 这5个阶段的先后次序是确定的,其余的都不是。
2.1 类加载过程
加载 :
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据
- 在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类的访问入口。
验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 文件格式验证:验证字节流是否符合class文件格式规范,并且能对当前虚拟机版本处理
- 元数据验证:对字节码描述的信息进行语义分析,保证符合java语言规范的要求
- 字节码验证:通过数据流和控制流进行分析,确定程序含义合法符合逻辑
- 符号引用验证:对类自身以外的信息进行匹配性校验(常量池中的各种符号引用)
准备:为类变量(static修饰的变量)分配内存和设置初始值(数据类型的零值)。
如果变量还加有final修饰,则准备阶段就将其值初始化为指定的值。
解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
- 类或接口
- 字段
- 类方法
- 接口方法
- 方法类型
- 方法句柄
- 调用点限定符
初始化:初始化是类加载的最后一步,初始化是执行类构造器<clinit>()的过程。
进行初始化的条件:
使用new 关键字实例化对象的时候,设置或读取一个类的静态字段(被final修饰除外,这个在编译期就已经放入常量池),调用一个类的静态方法
使用反射的时候
当初始化一个类时,父类还没有被初始化
当虚拟机启动时,用户指定执行的主类,虚拟机会先初始化这个主类
-
使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果ref_getStatic,ref_putStatic,ref_invokeStatic的方法句柄,如果这个类没有被初始化。
需要注意的是有且只有满足这五种条件中的一种才能进行初始化。
虚拟机规定,除了这五种场景之外所有的引用类都不会触发初始化,其它的引用都被称为被动引用。
可以分析下几段代码
package classLoadTest; /** * @author zhaokai008@ke.com * @date 2019-04-02 20:33 */ public class Super { public static int supT = 1; static { supT =2; System.out.println("super init"); } public static String value = "test"; public static final String finalTest= "finalTest"; } //***** package classLoadTest; /** * @author zhaokai008@ke.com * @date 2019-04-02 20:35 */ public class Sub extends Super { public static int Sub =supT; static { System.out.println("sub init "); } } //***** package classLoadTest; import org.junit.Test; /** * @author zhaokai008@ke.com * @date 2019-04-02 20:36 */ public class test { @Test public void testsub(){ System.out.println(Sub.value); } @Test public void finalTest(){ System.out.println(Super.finalTest); } @Test public void staticTest(){ System.out.println(Sub.Sub); } }
当执行testSub()方法的时候:
super init
test
可见子类并没有被初始化。因为通过子类来调用父类的静态变量时,父类满足上诉第一条,父类被初始化,而子类不满足上述五条的任何一条,所以不会输出:sub init
当执行finalTest()方法时:
finalTest
并没有输出 super init 。这是因为 finale 修饰的字段在准备阶段就已经被加入到test类常量池里面去了,而super类不满足上诉五种条件中的一种,所以不会被初始化。
当执行staticTest 方法时:
super init
sub init
2
这里sub 类也被初始化了,因为调用了sub类的静态变量,符合1,而值时2,说明了父类的初始化会在子类之前完成,这里验证了下面的2
初始化注意的点:
- Static{}静态语句中只能访问到定义在语句块之前的变量
- <clinit>()方法与类的构造函数不同,不需要显示的调用父类构造器。虚拟机会保证父类在子类之前完成,所以父类的静态语句块要先与子类执行
- <clinit>()对于类和接口不是必须的,没有静态语句块,没有对变量进行付值就不需要。
- 接口不能使用静态语句块
- 虚拟机会保证一个类的<clinit>()方法在多线程中被正确的加锁,同步。
2.2 类加载器
对于任何一个类都需要它的类加载器和这个类本身一同确定其在虚拟机中的唯一性。
从虚拟机角度来看只有两种类加载器:启动类加载器和其它。
从java开发者角度来说,有三种:启动类加载器,扩展类加载器,应用程序加载器
双亲委派模型 :
如果一个类加载器收到了类加载的请求,他首先会委派给父类加载器去完成,如果父类无法完成,则子类才会去尝试。