一、类加载过程
一个Java类从被加载到虚拟机内存到卸载位置,它的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。其中加载、验证、准备、解析、初始化就是类加载的过程。
1. 加载
查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象。
加载过程
在类加载的在加载阶段,虚拟机需要做
1.通过类的全限定名获取该类的二进制字节流
2.将二进制字节流所代表的静态结构转化为方法区的运行时数据结构
3.在内存中创建一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
获取二进制字节流的方式
class文件一般通过在硬盘上查找编译好的字节码文件通过IO读入,其他的获取字节流的方式如下:
1.从.jar、.war等压缩包中读取
2.从网络流中获取,如Applet
3.运行时计算生成 (动态代理,通过反射在运行时动态生成代理类)
4.由 JSP 编译生成对应的 Class 类
5.从数据库中读取,如 有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发
注意事项
非数组类与数组类加载比较
数组类
数组类本身不通过类加载器创建,它是由jvm运行时创建的,所以没有对应的class文件。
但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终是要靠类加载器去创建。
非数组类
非数组类加载可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器控制字节流的获取方式(如重写一个类加载器的 loadClass() 方法)。
类加载中加载和连接阶段的顺序
类加载中加载阶段与连接阶段的部分内容交叉进行,加载阶段尚未完成,连接阶段可能已经开始了。但这两个阶段的开始时间仍然保持着固定的先后顺序。
2. 验证
验证是连接阶段(验证、准备、解析、初始化)的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
对类的验证:文件格式验证、元数据验证、字节码验证、符号引用验证
注意事项
验证阶段对于虚拟机的类加载机制来说。不一定是必要的阶段。如果可以确认所运行的代码是安全的,可以使用 -Xverify:none参数来关闭大部分的类验证措施,可以缩短虚拟机类加载的时间。
3. 准备
准备阶段是类变量分配内存并赋予默认值的阶段。这里的类变量指的是被static修饰的变量,而不包括实例变量。类变量被分配到方法区中,而实例变量存放在堆中。
例如:
public static int number = 1;
在准备阶段过后,number的值为0,而不是1。复制为1的动作发生在初始化阶段。
基本数据类型初始默认值表
数据类型 | 默认值 |
---|---|
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
4. 解析
解析阶段是把类中的符号引用转换为直接引用。
5. 初始化
初始化阶段是类加载的最后一步,到这才真正开始执行Java代码。在准备阶段,已经为类变量分配内存,并赋值了默认值。在初始阶段,则可以根据需要来赋值了。可以说,初始化阶段是执行类构造器 < clinit > 方法的过程。
类构造器< clinit > 和实例构造器< init >区别
类构造器< clinit > 方法是在类加载的初始化阶段执行,是对静态变量、静态代码块进行的初始化。
实例构造器< init > 方法是new一个对象,即调用类的 constructor方法时才会执行,是对非静态变量进行的初始化。
执行顺序:
保证父类的 < clinit > 方法执行完毕,再执行子类的 < clinit > 方法。
由于父类的 < clinit > 方法先执行,所以父类的静态代码块也优于子类执行。
例如:
public class Demo1 {
static {
System.out.println("static block Demo1");
}
public static void main(String[] args) {
new A();
System.out.println("load test");
B b = null;
}
}
/**
* A的父类
*/
class AA{
static {
System.out.println("static block AA");
}
public AA(){
System.out.println("initial AA");
}
}
class A extends AA{
static {
System.out.println("static block A");
}
public A(){
System.out.println("initial A");
}
}
class B{
static {
System.out.println("static block B");
}
public B(){
System.out.println("initial B");
}
}
执行结果
static block Demo1
static block AA
static block A
initial AA
initial A
load test
二、类加载器
类加载就是将磁盘上的class文件加载到内存中。
类加载阶段的加载中"通过类的全限定名获取该类的二进制字节流 ",这个动作实在jvm外部实现的,这个实现的代码模块就是"类加载器"。
类加载器的分类:引导类加载、扩展类加载器、应用程序类加载
1. 引导类加载器(BootstrapClassLoader)
负责加载支撑jvm运行的位于jre的lib目录下的核心类库和jre下的classes
代码加载查看
/**
* 启动类加载器加载的类
*/
public static void bootClassLoaderLoadingPath() {
String bootStrapLoadingPath = System.getProperty("sun.boot.class.path");
List bootLoadingPathList = Arrays.asList(bootStrapLoadingPath.split(";"));
for (String bootPath : bootLoadingPathList) {
System.out.println("启动类加载器的目录:"+bootPath);
}
}
运行结果
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\resources.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\rt.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\sunrsasign.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\jsse.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\jce.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\charsets.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\jfr.jar
启动类加载器的目录:D:\Java\jdk1.8.0_291\jre\classes
2. 扩展类加载器(ExtensionClassLoader)
负责加载支撑jvm运行的位于JRE的lib目录下的ext扩展目录中的jre类包和sun目录下的ext的类包。
代码加载查看
/**
* 拓展类加载器加载的类
*/
public static void extClassLoaderLoadingPath() {
String extLoadingPath = System.getProperty("java.ext.dirs");
List extLoadingPathList = Arrays.asList(extLoadingPath.split(";"));
for (String extPath : extLoadingPathList) {
System.out.println("拓展类加载器的目录:"+extPath);
}
}
运行结果
拓展类加载器的目录:D:\Java\jdk1.8.0_291\jre\lib\ext
拓展类加载器的目录:C:\Windows\Sun\Java\lib\ext
3. 应用程序类加载器(AppClassLoader)
负责加载ClassPath路径下的类包,主要就是加载我们自己写的类。
代码查看
/**
* 应用程序程序类加载器加载的类
*/
public static void appClassLoaderLoadingPath() {
String appLoadingPath = System.getProperty("java.class.path");
List appLoadingPathList = Arrays.asList(appLoadingPath.split(";"));
for (String appPath : appLoadingPathList) {
System.out.println("应用程序加载器的目录:"+appPath);
}
}
4.自定义加载器(CustomClassLoader)
负责加载用户自定义路径下的类包,自己实现需要继承 java.lang.ClassLoader 类,通过该类的方式来实现。
三、双亲委派机制
jvm的类加载顺序是双亲委派机制,加载某个类时会先委托父类加载器查询是否加载过。
如果已加载则直接返回,如果没有加载则判断是否有父类加载器,如果存在父类加载器则由父类加载器判断是否加载,依次往上;如果没有父类加载器那么调用当前类加载器类进行加载,如果在当前的类加载没有加载到则让子类来自己加载,依次往下加载。
双亲委派机制简单来说就是,先找父亲加载,不行再由儿子自己加载;双亲委派简单理解:向上委派,向下加载。
1.双亲委派模式的源码
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载了该类
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();
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
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;
}
}
2.设计双亲委派模式的原因
沙箱安全机制
自己写的java.lang.String.Class类不会被加载,这样便可以防止核心API库被随意篡改。
避免类的重复加载
当父加载器已经加载了该类时,就没有必要在子类ClassLoader再加载一次,保证被加载类的唯一性。
3.打破双亲委派模式
类加载的双亲委派模式可以被打破吗?
答案肯定的,tomcat就就为了实现隔离性,打破了双亲委派模式。
三、全盘委托机制
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
全盘委托机制是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,否则该类所依赖及引用的类也由这个ClassLoder载入。