一、类加载时机
一个类从被加载到内存到卸载出内存为止,它的整个生命周期会经历加载、验证、准备、解析、初始化、使用、卸载这七个阶段。其中验证、准备、解析三哥部分统称为连接。
加载、连接、初始化、使用、卸载这五个步骤都顺序是固定的,但是再连接阶段,并不一定固定,验证、准备、解析是按步骤开始,但并不一定按该顺序结束,有可能穿插执行。
对于加载和连接在什么情况下进行,JVM虚拟机规范并没有进行强制约定,但是明确规定了类初始化的六个时机(有且仅有这六种情况)
1、遇到new、getstatic、putstatic或invokestatic这四条指令时,如果类型没有仅从过初始化,则需要先触发初始化阶段。可以生成这四条指令的典型Java代码场景有:
(1)使用new关键字实例化对象的时候
(2)读取或设置一个类的静态字段的时候(被final修饰、已在编译期把结果放入常量值的静态字段除外)
(3)调用一个类的静态方法的时候
2、使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。
3、当初始化的时候,如果发现父类还没有进行过初始化,则需要先触发其父类的初始化
4、当虚拟机启动的时候,用于需要指定一个要执行的主类,虚拟机会初始化该类
5、当使用JDK7新加入的动态语音支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getstatic、REF_putstatic、REF_invokestatic、REF_newInstanceSpecial四种类型的方法句柄,并且这个句柄对应的类没有被初始化,则需要先触发其初始化。
6、当一个接口中定义了JDK8新加入的默认方法时,如果有这个接口的实现类发生了初始化,那该接口要在其初始化前进行初始化。
二、类加载过程
类的加载过程是指的类的加载、验证、准备、解析、初始化这五个阶段。
(一)加载
加载主要做三件事情:
1、通过类的全限定名来获取定义此类的二进制字节流
2、将二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
对于要加载类的来源,JVM虚拟机规范并没有明确要求,其可以是从zip、jar、war压缩包中读取的,也可以是从网络中读取的,也可以是运行时计算得到的,也可以是由其他文件生成,甚至还可以是从数据库读取的等。
上面的过程是对于类的加载,如果是数组类型,如果数组的组件类型是引用类型,就需要递归的去加载,然后数组将被标识在加载该组件的类加载器的类名称空间上。
如果数组是非引用类型,java虚拟机将会把数组标记为与引导类加载器关联。
加载阶段与连接阶段的部分动作是交叉进行的,比如一部分字节码格式的校验,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分。
(二)验证
验证主要是确保Class文件的字节流包含的信息符合JVM虚拟机规范,保证这些信息被当作代码运行后不会危害虚拟机的安全。
验证主要包括对文件格式验证、元数据验证、字节码验证、符号引用验证。
1、文件格式验证
校验是否以指定的魔数开头
校验主次版本号是否在java虚拟机接受的范围之内
常量池的常量中是否有不支持的常量类型(检查tag标志)
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info类型的常量中是否有不符合UTF8编码的数据
........
2、元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合JAVA语言规范的要求,主要校验
这个类是否有父类(除Object类之外,所有的类都需要有父类)
这个类的父类是否继承了不允许被继承的类(final修饰)
如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(final方法)
3、字节码验证
字节码验证的目的是要通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
4、符号引用的验证
最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用[3]的时候,这个转化动作将在连接的第三阶段——解析阶段中发生
(三)准备--为static分配内存并设置初始值(1.7以前分配到方法区,1.7之后分配到堆)
准备阶段是正式为类中定义的变量(static修饰的类变量)分配内存并设置初始值的过程。在1.7之前会被分配到方法区,在1.7以后会被分配到堆中。
例如下面的代码,准备阶段后,赋的初始值为0
public static int i = 123;
但是这里并不包括被final修饰的static变量,因为使用final修饰的变量在编译的时候就会分配内存。
类变量会被分配到方法区中,而实例变量会随类被分配到堆中
(四)解析--符号引用转换为直接引用
解析是将常量池的符号引用转换为直接引用的过程。
解析动作主要针对接口或类、类方法、接口方法、字段这四类符号引用进行。分别对应常量池中的CONSTANT_Class_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_Fieldref_info四种常量类型。
1、类或接口解析
判断需要转化的类是数组类型还是普通对象类型的引用,然后进行不同的解析。
2、字段解析
首先查找当前类中是否有简单名称和字段描述符都与目标相匹配的字段,如果有则返回。
如果没有,则查找其实现的接口,按照接口的继承关系从上到下查看是否存在名称和描述符都与目标匹配的字段。
如果还没有,则查找其父类,从上至下查找。
3、类方法解析
对类中方法的解析和堆字段的解析差不多,差别就是判断该方法是处于类中还是处于接口中,如果是处于类中,那么先查找父类,再查找接口
4、接口方法解析
接口方法解析与类方法解析基本上一致,由于接口不会实现接口,音质不存在查找接口。
(五)初始化--方法调用
初始化就是调用类初始化方法,为static变量赋值的过程(在准备阶段已经赋了初始值,在初始化阶段要按照程序中的内容进行赋值),同时调用类中的静态代码块。
初始化方法是编译器自动收集类变量和静态代码块自动产生的,编译器收集的信息由在其在代码中的顺序界定,因此静态代码块中只能访问在其前面的类变量,而不能访问在其后面的类变量。
实例构造器需要显示的调用父类的构造器,但是类的初始化方法不需要调用父类的初始化方法,JVM虚拟机会确保在当前类初始化之前完成对父类的初始化,因此在JVM中第一个被初始化的类是java.lang.Object
如果一个类或者接口中既没有类变量也没有静态代码块,那么编译器就不会为该类生成初始化方法。
接口也需要通过初始化方法为接口中的类变量赋值。
虚拟机会保证在多线程环境下,一个类的方法被正确的加锁,以确保在同一类加载器中,一个类只会被初始化一次。
三、类加载器
(一)类加载器
类加载器分为启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器。
启动类加载器(Bootstrap ClassLoader):负载JAVA_HOMR/lib目录中的、或通过-Xbootstrap参数指定路径中的且被虚拟机认可的类,该类加载器由C++实现,不是ClassLoader的子类
扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME/lib/ext目录中的、或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库
JVM的类加载器通过ClassLoader及其子类完成,类的层次关系和加载顺序可以由下图来描述:
加载过程是自下而上的逐层检查,看是否已经被加载,只有有一个ClassLoader加载了该类,就认为该类已经被加载,一个类只会被一个类加载器加载,且只会被加载一次。
而加载的顺序则是自上而下的。
(二)双亲委派
双亲委派:当一个类加载器收到一个类的加载任务时,其会先将类交给父类的加载器进行加载,因此最终会将加载任务传递到启动类加载器进行加载;只有当父类不加载该类时,当前加载器才能加载该类。
双亲委派的好处:可以保证同一个类无论被哪个类加载器加载,得到的都是同样的类对象。在JVM搜索类时,只有类和类加载器都相同,才认为是相同的类,那么双亲委派机制保证了同一个类,无论被哪个类加载器加载,最后都是使用相同的类加载器进行加载。例如java.lang.Object类,如论被哪个类加载器加载,最终都会被启动类加载器进行加载。
为什么要使用双亲委派:这样可以避免重复加载;从安全角度考虑,如果不适用双亲委派,我们就可以使用自定义的对象来动态替换java核心API中提供的类型,例如我们自己定义一个String类。而是用双亲委派,就可以避免这种情况,因为String类已经被启动类加载器加载,因此用户自定义的类加载器永远也不会加载到。
为什么要自定义类加载器:JVM中提供的默认的ClassLoader是加载指定位置的class文件,如果我们想加载其他位置的class文件,就需要我们自定义类加载器。例如加载网络上的class文件。
(三)破坏双亲委派
双亲委派模型是在JDK1.2之后才有的,但是有的类在JDK一开始就有了,因此某些情况下父类加载器需要加载的class文件由于受到加载范围的影响,不得不委托子类进行加载。
而按照双亲委派模式的话,是子类委托父类进行加载,这时候需要破坏双亲委派才能加载成功父类要加载的类。
以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了 MySQL Connector ,这些实现类都是以jar包的形式放到classpath目录下。类的全限定名完全相同,但是加载它的类加载器不同,那么在方法区中会产生不同的【Class】对象。DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类(classpath下),然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。