‘深入理解Java虚拟机-类的加载’
类加载过程
当程序主动使用某个类时(类加载是一种懒加载),如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。
加载(loading)
类的加载过程是指类加载器尝试加载class二进制文件,并在JVM中生成对应的数据结构,然后使其分布在JVM对应的内存区域
,并在堆内存中创建一个Java.lang.Class 对象,无论类加载多少次只会有一个class对象(-XX:+TraceClassLoading 追踪类加载信息并打印出来)(javap -c xx.class 可以反编译查看字节码)
调用Java类的方法分为主动使用和被动使用,被动使用不会导致类的初始化
- 通过new关键字会导致类的初始化
- 访问类的静态变量,包括读取和更新会导致类的初始化
- 访问类的静态方法会导致类的初始化
- 对某个类进行反射操作会导致类的初始化
- 初始化子类会导致父类初始化
- 启动类(main方法)所在类会被初始化
被动使用列如
构造某个类的数组时不会导致类的初始化
public class Test05 {
public static void main(String[] args) throws ClassNotFoundException {
//Parent类不会被初始化
Parent[] parent = new Parent[10];
System.out.println(parent.length);
}
}
引用类的简单静态常量不会导致类的初始化 (在编译过程就会放到类的常量池中,直接获得常量的值,本质上没有直接引用到该类)
public class Simple {
static{
System.out.println("进行初始化");
}
//访问静态常量MAX不会导致Simple类的初始化
public final static int MAX = 10;
//虽然RANDOM是静态常量,但是由于计算复杂,只有初始化后才能得到结果,因此其他类使用RANDOM会导致Simple类的初始化
//必须要在编译时就能获得具体的值
public final static int RANDOM = new java.util.Random(10).nextInt();
}
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
- 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
- 从JAR/ZIP包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从-- JAR文件中直接加载该class文件。
- 通过网络加载class文件。
- 从数据库中读取到class文件
- 把一个Java源文件动态编译,并执行加载。(jsp -->serlvet: serlvet 也是一个class文件)
================================================================================================
linking 连接
验证
验证的目的是为了保证class文件 符合Java的规范,列如我们随便去创建一个文件并把后缀改成 (点).class文件,为了保证 JVM的正常运行,因此JVM就会检验Class文件字节流中的魔数因子(cafebabe)来判断是否为一个Java编译过后的class文件。
- 验证文件格式:包括文件头部的魔数因子、class文件主次版本号、class文件的MD5指纹、变量类型是否支持等
- 元数据的验证:对class的字节流进行语义分析,判断是否符合JVM规范。简单来说就是java语法的正确性
- 字符码验证:主要验证程序的控制流程,如循环、分支等
- 符号引用验证:验证符号引用转换为直接引用时的合法性,保证解析动作的顺利执行。比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。
准备
准备阶段主要做的事就是在方法区为静态变量发配内存以及赋初始默认值类如int 类型赋予0 ,boolean = false(区别于初始化阶段赋的程序指定的真实值)
注意:final修饰的静态常量在编译阶段就已经赋值,不会导致类的初始化,是一种被动引用,因此也不存在连接阶段。
解析
解析就是在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程。
解析过程主要针对类接口、字段、类方法和接口方法四类进行。
init 初始化
初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行 class 文件反编译中的< clinit>(),该方法会为所有的静态变量赋予正确的值。
关于静态变量赋值注意以下几点:、
静态语句块可以对后面的静态变量赋值,但不能对其进行访问。代码示例:
public class Test {
static{
System.out.println(x);//此处不能通过编译
x=100;
}
private static int x = 10;
}
父类静态变量总是能优先赋值。代码示例:
public class Parent02 {
static int x = 10;//①
static{ x = 20; }//②
}
public class Child02 extends Parent02{
static int i = x;
public static void main(String[] args) {
System.out.println(Child02.i);//输出20
}
}
父类优先执行< clinit>()方法,x被赋值20。所以程序输出20.
注意:如果以上代码中①行和②行位置调换,则程序输出10。准备阶段在方法区分配x的内存,值为0,初始化阶段< clinit>()方法顺序执行。
类加载机制和加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的(为了解决这个问题用到双亲加载机制)。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
1)根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
public class bootstrapsloader {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.toExternalForm());
}
}
}
2)扩展类加载器(extensions class loader):它负责加载JRE(Java运行时环境)的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
类加载器加载Class大致要经过如下8个步骤:
- 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
- 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
- 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
- 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
- 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
- 从文件中载入Class,成功后跳至第8步。
- 抛出ClassNotFountException异常。
- 返回对应的java.lang.Class对象。
JVM的类加载机制主要有如下3种。
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
双亲委派机制:
为了防止多个类加载器对一个类进行加载,(自己不去加载让父类加载)
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务(就会返回一个异常),子加载器才会尝试自己去加载。
红色为无法加载时抛出异常,让他的子类去加载
双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。