1 类的加载
类的加载是指将类的.class文件读取进内存中,并将其放在JVM运行时数据区的方法区内,然后在堆中创建一个java.lang.Class对象,用于封装类在方法区中的数据结构,同时作为方法区数据的访问入口。
2 类的生命周期
类的生命周期指一个class文件从加载到卸载的整个过程
3 类的加载过程
JVM将类的加载分为三个阶段:装载(Load)、链接(Link)、初始化(Initialize)
3.1 装载:查找并加载类的二进制数据
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
3.2 链接:验证、准备、解析
- 验证
校验字节码文件的正确性、安全性,包括四种验证:文件格式验证、元数据的验证、字节码验证、符号引用验证 - 准备
为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,这里不包含final修饰的static,因为final在编译的时候就已经分配了。也不会为实例变量分配初始化,实例变量会随着对象分配到堆内存中。 - 解析
把常量池中的符号引用转换为直接引用,静态链接过程
3.3 初始化:初始化静态变量,执行静态代码块
3.3.1 类的初始化时机
如果一个类被直接引用,就会触发类的初始化。在Java中,直接引用的情况有:
● 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法
● 通过反射执行以上三种行为
● 初始化子类的时候,会触发父类的初始化
● 作为程序的入口直接运行时(main方法)
# 类初始化时机DEMO
class InitClass {
static {
System.out.println("class init");
}
public static String data = null;
public static void method (){};
}
class SubInitClass extends InitClass{}
public class TestInitClass{
// 类的初始化4:main方法运行,触发类初始化
// 初始化仅会进行一次,所以main方法中只会输出一次 "class init"
public static void main(String[] args) throws Exception{
// 类初始化1:new对象、读取或设置类静态变量、调用类的静态方法
new InitClass();
InitClass.data = "";
String data = InitClass.data;
InitClass.method();
// 类初始化2:反射
Class<InitClass> clazz = InitClass.class;
clazz.newInstance();
Field field = clazz.getDeclaredField("data");
field.get(null);
field.set(null, "s");
Method method = clazz.getDeclaredMethod("method");
method.invoke(null,null);
// 类的初始化3:实例化子类,引起父类实例化
new SubInitClass();
}
}
3.3.2 类初始化步骤
- 假如这个类还没有被加载和链接,则程序先加载并连接该类(动态加载:主类在运行过程中如果使用到其他类,会逐步加载这些类 eg:jar包和war包中的类并不是一次性都加载到内存中,而是使用时才加载)
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
# 动态加载DEMO
public class TestDynamicLoad {
static {
System.out.println("load TestDynamicLoad");
}
public static void main(String[] args) {
// new触发类的加载
new A();
// 空引用不触发类的加载
System.out.println("TestDynamicLoad main");
A a = null;
// 调用静态常量,不触发类的加载
System.out.println("TestDynamicLoad main2");
System.out.println(A.NAME);
// 调用静态变量,触发类的加载
System.out.println("TestDynamicLoad main3");
System.out.println(A.name);
}
}
class A{
public static final String NAME = "a";
public static String name = "aName";
static {
System.out.println("load A");
}
public A(){
System.out.println("initial A");
}
}
4 类加载后方法区存储内容
类被加载到方法区中后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。
类加载器的引用:
这个类到类加载器实例的引用
对应class实例的引用:
类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
5 类加载器
类加载过程主要通过类加载器实现,Java中有以下几种类加载器:
- 引导类加载器(Bootstrap ClassLoader)
负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar\charsets.jar等,由C++实现,不是ClassLoader子类,Java程序无法直接引用。 - 拓展类加载器(Extension ClassLoader)
负责加载支撑JVM运行的位于JRE的lib目录下的ext拓展目录中的类库 - 应用类加载器(App ClassLoader)
负责加载ClassPath路径下的类包,主要加载开发人员写的类 - 自定义类加载器(Custom ClassLoader)
负责加载用户自定义路径下的类包
类加载器之间存在父子层级结构:
# classloader父子层级DEMO
public static void main(String[] args) {
ClassLoader app = ClassLoader.getSystemClassLoader();
ClassLoader ext = app.getParent();
ClassLoader boot = ext.getParent();
System.out.println("the bootstrapLoader: " + boot);
System.out.println("the extClassLoader: " + ext);
System.out.println("the appClassLoader: " + app);
}
#结果
the bootstrapLoader: null // c++实现,java获取不到引用
the extClassLoader: sun.misc.Launcher$ExtClassLoader@6e0be858
the appClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
6 双亲委派机制
6.1 定义
双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的加载路径下找不到指定类时,子类才会尝试自己去加载。
6.2 工作过程
- 当应用程序类加载器收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
- 当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
- 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
- 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
- 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
- 如果均加载失败,就会抛出ClassNotFoundException异常。
6.3 双亲委托机制实现源码(JDK 1.8)
6.4 双亲委托作用
- 沙箱安全机制:防止核心类库被篡改,自己写的java.lang.String不会被加载。
- 避免类的重复加载:当父加载器已经加载过该类时,子加载器就没必要重新加载一次,保证被加载类的唯一性。
# 自定义java.lang.String,运行异常,查找不到main方法
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("My String Method");
}
}
7 自定义类加载器
/**
* 自定义类加载器
* 1. 继承ClassLoader父类,实现自定义findClass方法
* 2. findClass职责:加载class类到内存,并转换为Class类
**/
public class MyClassLoader extends ClassLoader{
private final String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
/**
* 通过类全限定名加载类文件byte数组
* @param name 加载类路径
* @return class文件字节数组
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.","/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int length = fis.available();
byte[] data = new byte[length];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
8 如何打破双亲委派机制
核心:继承ClassLoader,重写类加载方法,实现加载逻辑,不委托给双亲加载
/**
* 自定义类加载器
* 1. 继承ClassLoader父类,实现自定义findClass方法
* 2. findClass职责:加载class类到内存,并转换为Class类
**/
public class MyClassLoader extends ClassLoader{
private final String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
/**
* 通过类路径加载类文件byte数组
* @param name 加载类路径
* @return class文件字节数组
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.","/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int length = fis.available();
byte[] data = new byte[length];
fis.read(data);
fis.close();
return data;
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委托给双亲加载
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 定义某些类不走双亲委派机制,直接进行类的加载
if (name.startsWith("com.matt.learn")){
c = findClass(name);
}
else {
c = this.getParent().loadClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
拓展
Tomcat类加载机制实现:Tomcat如何保证不同的webapp应用中加载不同版本类库?JSP热加载机制?
参考文档:
https://www.cnblogs.com/ityouknow/p/5603287.html
https://blog.csdn.net/zhengzhb/article/details/7517213