类加载机制:虚拟机把描叙类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类加载的生命周期包含:加载、验证、准备、解析、初始化、使用、卸载。其中,验证、准备、解析3个部分称为链接。
虚拟机对于类的初始化阶段严格规定了有且仅有只有5种情况如果对类没有进行过初始化,则必须对类进行“初始化”!
除此之外,所有的引用类的方法都不会触发初始化,称为被动引用。被动引用的例子如下:
public class SuperClass {
static {
System.out.println(" supser class init");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println(" subclass init ");
}
}
public class NoInitialization1 {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
输出结果:
supser class init
123
public class NoInitialization3 {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[3];
}
}
没有任何输出
这段代码触发了一个[LSuperClass]的类的初始化阶段,是由虚拟机自动生产的、直接继承java.lang.object的子类,创建动作由字节码指令newarray触发。
public class ConstClass {
static {
System.out.println(" Const class init !");
}
public static final String HELLOWORLD = "hello world";
}
public class NoInitialization2 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
接口的加载过程和类不同,只有在:当一个类初始化的时候,要求其父类全部都已经初始化过了,但是一个接口在初始化时候,并不要求其父接口都完成初始化,只有真正使用父接口(如:引用接口定义的常量)的时候才会初始化。
类的加载过程也就是加载、验证、准备、解析以及初始化。
注意“加载”和“类加载”的区别。
在加载的阶段,虚拟机只需要完成以下三件事:
验证是链接的第一步,这一阶段的目的是为了保护class文件的字节流的信息符合虚拟机的要求,并且不会危害虚拟机自身的安全。验证如果检查不符合class文件的格式约束,虚拟机就应抛出一个java.lang.VerifyError异常或者子异常。验证大概分为4个验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证字节流是否符合class文件格式规范,并且能被当前版本的虚拟机处理。这一阶段可能验证:
对字节码描叙的信息进行语义分析,保证其描叙信息符合java语言规范的要求。这些阶段可能包含验证点如下:
这一阶段主要目的是通过数据流和控制流分析,确定程序的语义是否合法,是否符合逻辑。这阶段分析对类的方法体进行校验分析,保证被校验类的方法体在运行中不会作出危害虚拟机安全的事件:
在jdk1.6之后javac编译器和java虚拟机,给方法体Code属性的属性表中增加了一项名为“StackMapTable”的属性,这描叙了方法体中所有基本块开始本地变量表和操作栈应有的状态,检查StackMapTable属性中的记录是否合法即可。
最后一阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,发生在解析阶段。符合引用验证可以看做是对类自身意外的信息进行匹配性校验,需要校验以下内容:
1. 符合引用中通过字符串描述的全限定名是否能够找到对应的类
2. 在指定类中是否存在符合方法的描述符以及简单名称所描述的方法和字段
3. 符合引用中的类、字段、方法的访问属性(private、protected、public、default)是否可被当前类访问
……
准备阶段是正式为类变量(static修饰的)分配内存并设置类变量初始化值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里飞蛾复制通常是数据类型的零值。
public static int value = 123;
变量在准备阶段过后的初始值为0而不是123,把value设置成123的putstatic指令,存放在()方法中,把value赋值123,在初始化阶段才执行。public static final int value = 123;
如果类字段的字段属性(ConstantValue)属性,那么准备阶段的值为属性的值。解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
#####符号引用和直接引用
符号引用:符号引用是一组符合来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用的字面量形式明确定义在java虚拟机规范的class文件格式中
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7种引用进行,分别对应CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info7种类型。
对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:
public class Test{
public static void main(String[] args){
new C();
}
}
class A{
public A(String name){
System.out.println(name + " A constructor");
}
}
class B{
private A = new A("b");
public B(){
System.out.println("B constructor");
}
}
class C extends B{
private A = new A("c");
public C(){
System.out.println("C constructor");
}
}
输出:
b A constructor
B constructor
c A constructor
C constructor
通过这个例子以及字段解析的过程,我们可以更深刻理解为什么在具有继承关系的类中,为什么总是先加载父类的构造方法以及初始化,然后才调用子类的构造方法以及初始化。
进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
到了初始化阶段,虚拟机才开始真正执行Java程序代码,前文讲到对类变量的初始化,但那是仅仅赋初值,用户自定义的值还没有赋给该变量。只有到了初始化阶段,才开始真正执行这个自定义的过程,所以也可以说初始化阶段是执行类构造器方法的过程。那么这个 方法是这么生成的呢?
() 是编译器自动收集类中所有类变量的赋值动作和静态语句块合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序决定的
() 方法与类的构造器方法不同,因为前者不需要显式调用父类构造器,因为虚拟机会保证在子类的() 方法执行之前,父类的 方法已经执行完毕
由于父类的 方法会先执行,所以就表示父类的static方法会先于子类的 方法执行。这点也可以通过下面的代码得到体现:
static class A{
public static int a = 1;
static{
a = 2;
}
}
static class B extends A{
public static int b = a;
}
public static void main(String[] args){
System.out.println(B.b);
}
得到的结果是2而不是1,这就验证了父类的静态方法会先于子类的static方法执行。
类加载器虽然用于实现类的加载,在java程序中起到的作用却不止类的加载阶段。对于任意一个类,都需要由加载器和这个类一同确立其在java虚拟机的唯一性,没有类加载器都有唯一的空间。
package com.own.learn.jdk.cls1.classLoading;
import java.io.InputStream;
public class ClassLoadTest {
public static void main(String[] args) throws Exception {
final ClassLoader classLoader = new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
final InputStream is = this.getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
final byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
}
return super.loadClass(name);
}
};
final Object o = classLoader.loadClass("com.own.learn.jdk.cls1.classLoading.ClassLoadTest").newInstance();
System.out.println(o.getClass());
System.out.println(o instanceof com.own.learn.jdk.cls1.classLoading.ClassLoadTest);
}
}
输出:
class com.own.learn.jdk.cls1.classLoading.ClassLoadTest
false
从java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),由C++语言实现的,是虚拟机的一部分。另一种是所有其他类加载器,这些类加载器都由java语言的实现按,独立虚拟机外部,并且全部继承抽象类java.lang.classloader。类加载器分为3种:
类夹杂其之间的这种层次关系,称为类加载器的双亲委派模型。
双亲委派模型工作过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个类加载器都是如此,因此所有的类记载器请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
好处:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
Java自定义类加载器与双亲委派模型