类加载机制:jvm把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类加载的时机
类加载的生命周期:
其中,加载、验证、初始化和卸载的顺序是固定的。而解析过程可以在初始化之前或者之后开始,而如果解析在初始化之后开始的的目的是支持java的运行时绑定特性。
初始化的时机
首先,初始化必须在加载、验证、准备之后进行。
- 遇到new、getstatic、putstatic或者invokestatic这四条字节码时。
具体场景有:
- 使用new关键字实例化对象
- 读取或者设置一个类的静态字段(static关键字修饰),但是被final、已在编译期就把结果放入常量池的静态字段除外
- 调用一个类的静态方法
- 使用java.lang.reflect包的方法对类进行反射调用时。
- 在初始化这个类时,如果其父类没有被初始化就要把父类先初始化
- 虚拟机启动时初始化含有main方法的主类
- 动态语言支持:如java.lang.invoke.MethodHandle,如果MethodHandle解析的结果指向了静态字段或者静态方法,那么这个类就要被初始化
- 如果实现类初始化了,那么含有default方法的接口也有被初始化
这6种称为主动引用,除此之外,所有的引用类型都不会触发初始化,称为被动引用
被动引用
- 调用子类引用父类的静态字段,只会导致父类初始化,而不会导致子类初始化。
- 通过数组引用类,不会初始化类。例如NB[] nb = new NB[10]不会初始化NB类。
- 调用final修饰的静态字段的类不会被初始化,例如static final String s = "hello",定义这个字段的类不会被初始化。原因是被final修饰的字段在编译阶段通过常量传播优化,直接存储在了调用类的常量池中了。
接口与类的区别
如果是类的话,子类被初始化而父类一定会被初始化,但接口不需要立刻初始化父类,真正用到父接口时才会初始化父接口
类加载的过程
加载
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成 代表这个类的java.lang.Class对象作为方法区中这个类的各种数据的访问入口
对于数组类而言,数据类不通过类加载器创建,而是由java虚拟机直接在内存中动态构造出来的。但数组的元素类型(如Student[]的元素类型就是Student)是由类加载器加载的
- 如果数组的元素类型是引用类型,就递归加载这个组件类型(例如String[][],就递归加载到String),并在该组件类型的类加载器的类名称空间上添加这个数组的标识
- 如果数组的元素类型不是引用类型,例如int,jvm就把数组标记为与引导类加载器关联。
- 数组类的可访问性与它的元素类型的可访问性一致,如果元素类型是int等非引用类型,就默认public
验证
作为连接阶段的第一步,作用是确保Class文件的字节流信息符合规范,不会危害虚拟机安全。
- 文件格式验证(Class文件格式验证):检验字节流是否符合文件格式规范,例如魔数,版本号,常量类型等等。保证输入的字节流能正确地解析并存储在方法区中。
- 元数据验证(字节码语义验证):对字节码信息进行语义分析,包括这个类是否有父类,父类是否被final修饰,抽象类是否实现了父类的所有方法,类中的字段是否与父类冲突。
- 字节码验证(程序语义验证):如果数据流和控制流分析,确定语义是否合法、合逻辑,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如跳转指令跳转到了方法体之外,参数转化是否正确,参数类型是否正确。
- 符号引用验证(类的正确性验证):发生在符号引用转化为直接引用的时候(解析阶段),验证类自身以外的各类信息,即类依赖的外部类、方法、字段以及其可访问性(public,private,protected)的检查等。
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量的初始值。
这里只分配类变量,并不分配实例变量,实例变量在对象实例化时随着对象分配在java堆中(2.3.1)
初始值指的是数据类型的零值,例如static in value = 123;value在准备阶段的值为0,将value赋值为123的putstatic指令是在程序被编译后在<clinit>()方法中,在初始化阶段才会执行。
基本数据类型的零值
解析
jvm将常量池的符号引用替换为直接引用
发生的时机可能是类加载器加载时,也可能是符号引用被使用前,有可能在运行时才会解析。
同一个符号引用可能被多次解析,所有jvm对解析的结果可以缓存在内存中。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,引用的目标不一定是已经加载到虚拟机内存当中的内容。
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄。引用的目标一定在虚拟机的内存中。
主要包括以下几类:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
符号引用在类加载后存放在方法区的运行时常量池中,符号引用翻译出来的直接引用也存储在运行时常量池中。
初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
- <clinit>()方法是由编译器收集类中的所有类变量的赋值操作和静态语句块(static)产生的,收集顺序是由在文件中出现的顺序决定的,所以static不能访问在static后面定义的变量。
- 虚拟机保证在子类的clinit方法执行前其父类的clinit方法已经执行,所以最先执行的clinit方法是java.lang.Object类
- clinit方法对于类和接口来说不是必须要的,没有static静态代码块和对变量的赋值操作的话可以不生成clinit方法。
- 接口不能使用静态代码块,但仍然有变量初始化的赋值操作同样会生成clinit方法。但接口不需要像类一样先初始化父类(即执行clinit方法),而是只有这个变量被使用时才会初始化。
- 类只能被初始化一次,如果有多个线程想初始化一个类,只有一个能获取锁,其他线程等待执行,并且其他线程阻塞被唤醒后仍然不能初始化这个类,因为这个类已经被初始化过了。