从类加载机制到热加载
从类加载机制到热加载
类是如何加载的
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入 口。
验证
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
- 文件格式验 证验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
- 元数据验证 字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
- 字节码验证 通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的
- 符号引用验证 该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源
准备
正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
- 对static修饰的变量初始化
- 如果初始化时父类没初始化 初始化父类
- 执行静态代码块
在验证、准备、解析、初始化我们能介入的空间不多,想实现热加载这个阶段只能在加载这步想办法
ClassLoader
类都是由ClassLoader进行加载的,这里先介绍最重要的双亲委派机制
启动类加载器负责加载<JAVA_HOME>\lib目录下的类或者被-Xbootclasspath参数所指定的路径中存放的,而且必须是Jave虚拟机能够识别的(及即使自己新建一个类在\lib目录下也不会被加载)
扩展类加载器加载<JAVA_HOM E>\lib\ext目录下的类
启动类加载器则是加载我们自己(ClassPath)写的类
在加载类时不本身类不会自己先加载而是由自己的父类加载器去加载
注意:这里不是继承的关系而不是组合的关系。
具体的代码在sun.misc.Launcher
类中
当加载时调用java.lang.ClassLoader#loadClass(java.lang.String, boolean)
方法对类进行加载
实现热加载
首先我们要知道类是否相同除了class必须相同类加载器也需要相同才为会认为两个类是相同的,并且我们需要打破双亲委派机制如果让父加载器先加载,当修改class后父类加载器是不会重新加载的
那我们新建一个自己的classLoader
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
//父类构造方法 会把这个类的父加载器设置为getSystemClassLoader()也就是应用类加载器
super();
this.classPath = classPath;
}
//把类文件转换为数组
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
byte[] data = new byte[fis.available()];
fis.read(data);
fis.close();
return data;
}
//loadClass会调用此方法获取class
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadByte(name);
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
long t1 = System.nanoTime();
// 只加载自己写的类对于其他类还由父类进行加载,否则会报错
if(!name.startsWith("ingxx")){
c = this.getParent().loadClass(name);
}else {
c = findClass(name);
}
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
Main方法
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, InterruptedException {
while (true) {
MyClassLoader myClassLoader = new MyClassLoader("/Users/wjk/test/");
Class<?> aClass = myClassLoader.loadClass("ingxx.user.User");
Object o = aClass.newInstance();
Method method1 = aClass.getMethod("sout", null);
method1.invoke(o, null);
Thread.sleep(5000);
}
}
新建一个测试类
public class User {
public void sout(){
System.out.println("我是李四");
}
}
编译后把class文件放到/Users/wjk/test/ingxx/user目录下
启动后打印"我是李四" 把User
类的"我是李四"改为"我是张三"放到/Users/wjk/test/ingxx/user目录下 打印结果如下
如果用同一个类加载器重复加载会抛异常如下图所示
新建类加载器在实际中的应用
Tomcat
不难想象在tomcat中我们每个war包可能有相同的类。
例如我们有两个war包一个war包依赖的Spring是4.0另一个war包以来的是5.0,如果Tomcat都使用应用类加载器加载,那么到底加载的是哪个版本呢?为了结局这个问题Tomcat自定义了一些类加载器
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
真对每个webApp都有自己的类加载器从而实现了类隔离
jsp类加载器针对每个jsp文件生成一个类加载器 当jsp类更改时会new一个新的jsp类加载器对jsp进行重新加载
依赖冲突
在阿里内部有一个潘多拉可以实现类隔离,保证第三方包和第二方包存在同样的类但是版本不同时也不会发生java.lang.ClassNotFoundException