一、类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转換解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类的加载机制可以分为加载-链接-初始化三个阶段,链接又可以分为验证、准备、解析三个过程,并且加载、链接、初始化的各个阶段并不是彼此独立,而是交叉进行,比如会在一个阶段执行的过程中调用/激活另外一个阶段。
1、加载
加载指的是把从各个来源得到的class字节码文件通过类加载器装载入内存中,并且会在堆内存中的方法区生成一个代表这个类的 java.lang.Class 对象,作为这个类的数据请求入口。这里需要了解两个知识点:
-
来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
-
类加载器:从JVM的角度,类加载器是分为了启动类加载器和其他类加载器(包括扩展类加载器,应用类加载器,以及用户的自定义类加载器)两种
-
启动类加载器(Bootstrap ClassLoader):
Bootstrap 类加载器负责加载< JAVA_HOME >/lib/ rt.jar 中的 JDK 类文件,它是所有类加载器的父加载器,一般是C++实现的,我们日常用的Java类库比如String, ArrayList等都位于该包内。Bootstrap 类加载器没有任何父类加载器,如果调用 String.class.getClassLoader(),会返回 null,任何基于此的代码会抛出 NUllPointerException 异常。 -
扩展类加载器(Extension ClassLoader):Extension 类加载器将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或 java.ext.dirs 系统属性定义的目录下加载类。Extension 由加载器sun.misc.Launcher$ExtClassLoader 实现。
-
应用程序类加载器(Application ClassLoader):默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类(System)加载器,实现类是sun.misc.Launcher$AppClassLoader。它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。
-
自定义类加载器(User ClassLoader):如果以上类加载起不能满足需求,可自定义类加载器。比如说App安全防护时加的壳,如果需要对自己的代码做防护,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。几种类加载器的关系如下:
-
延伸:每个类加载器都拥有一个独立的类名称空间,它不仅用于加载类,还和这个类本身一起作为在JVM中的唯一标识。所以比较两个类是否相等,只要看它们是否由同一个类加载器加载,即使它们来源于同一个Class文件且被同一个JVM加载,只要加载它们的类加载器不同,这两个类就必定不相等。
2、链接
1.验证
主要是对一些词法、语法进行规范性校验,避免对 JVM 本身安全造成危害。
2.准备
准备阶段主要是为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆。 比如 static int a=1,会被初始化成成 a=0;如果是 static double a =1,则会被初始化成 a=0.0,final static tmp = 1, 那么该阶段tmp的初值就是1。
3.解析
将常量池内的符号引用替换为直接引用的过程。在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
3.初始化
对类的静态变量和静态块中的变量进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
初始化阶段和准备阶段的区别是准备阶段的静态变量赋初始零值,而初始化阶段会根据Java程序的设定去初始化类变量和其他资源。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
- 在遇到new、getstatic、putstatic或invokestatic这4个字节码指令时必须立即对类进行初始化
二、双亲委派模型
简单来说包含如下几步:
1.判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次;
2.如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象;
3.在父ClassLoader没有加载成功的前提下,自己尝试加载类;
为什么要先让父ClassLoader去加载呢?其实这样可以避免Java类库被覆盖的问题,比如用户程序也定义了一个类java.lang.String,通过双亲委派机制,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。
三、类加载顺序
看一下以下程序的执行顺序:
class A{
public int i = method();
public static int j = method2();
public A(){
System.out.println(1);
}
public int method(){
System.out.println(2);
return 2;
}
public static int method2(){
System.out.println(3);
return 3;
}
}
class B extends A {
public int m = method3();
public static int n = method4();
public int t = 0;
public B() {
System.out.println(4);
}
public int method3() {
System.out.println(5);
return 5;
}
public static int method4() {
System.out.println(6);
return 6;
}
}
class People{
}
public static void main(String[] args) {
System.out.println(7);
A a = new TestDemo2();
A a1 = new B();
}
前面在讲初始化时提到当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类,我们会进行类的初始化。在代码中只有一个构造方法,但实际上Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和对象初始化方法 。
- 类初始化方法:编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
- 对象初始化方法:编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象(也就是new)的时候会立即执行。
因此上面程序的最终输出为:
3
7
2
1
6
2
1
5
4