在理解类加载前, 我觉得我们应该要先知道为什么我们写的代码可以被执行, 看下图:
我们写的代码编译成.class文件后, 被classloader加载到内存中, 同时也会把java自带的类库load到内存中
然后会编译器进行编译, 最后通过执行引擎执行
这里的编译会有三种模式:
纯解释模式 (启动快, 执行慢)
只使用字节码解释器 (Bytecode Intepreter)一条一条的读取, 解释并执行字节码命令;
纯编译模式 (启动慢, 执行快)
只使用 JIT(Just In-Time compiler)即时编译器把字节码编译成本地代码;
混合模式 (默认)
起始阶段采用纯解释执行, 热点代码使用编译器编译成本地代码执行
扩展 (怎么才会是热点代码)
1. 多次被调用的方法 (通过方法计数器监测) 2. 多次被调用的循环(通过循环计数器监测频率)
当然我们可以通过命令来指定某种模式
- -Xmixed (default) : 混合模式
- -Xint : 纯解释模式
- -Xcomp : 纯编译模式
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
就是把一个.class文件load到内存中, load进去后在内存中创建了两块内容, 一块存储了这个.class文件的二进制内容, 二是创建了这个Class的对象, 并且将这个对象指向了第一块内容
校验你这个.class文件内容是否合法
会在这个阶段为你的静态变量赋初始值
在这个阶段将类、方法、属性等符号引用解析为直接引用(指针、偏移量等内存地址)
上图中当要加载某一个类时, 自下向上 检查该类是否已经加载, 又自上而下进行实际的查找和加载, 这就是双亲委派, 继续看图
当某个类加载器接收到类加载请求时, 会先去自己的缓存中监测有没有, 没有就交给自己的父加载器, 依次类推, 直到查找到返回为止;
如果BootStrap也没查找到, 就会进行加载, 当BootStrap发现这个类不归它管时, 则交给子加载器加载, 依次类推, 直到加载到返回为止;
如果都没加载上, 就抛出ClassNotFoundException以后看见这个异常就别怕了
举个例子:
java.lang.String类, 我们知道这个Java自带的String位于rt.jar中, 那它的加载将经过Custom、Application、ExtensionClassloader, String这个类都不归前面三个加载器负责, 最终由Bootstrap加载并返回;
最主要的原因是为了安全
假设如果没有双拼委派会怎样?
我们是不是可以自定义一个java.lang.String类, 然后做一些特殊的处理, 比如说记录一下网站登录时输入的银行卡卡号与密码, 然后把这个类打包成一个jar, 然后在系统中调用, 然后会有两种结果, 1. 福布斯排行榜上会有你的名字; 2. 能吃上免费早中晚饭
其次是节约资源
这个没啥好说的, 加载一次后就不会再加载第二次了
可以看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
}
if (c == null) {
// 如果仍然找不到,请调用findClass以便找到该类。
long t1 = System.nanoTime();
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;
}
}
其实我们可以继承ClassLoader, 重写他的loadClass方法
思路:
原版有一个判断是不是已经加载过了, 而自定义的没有
判断这个类存不存在, 存在直接加载, 不存在就让父加载器去加载, 如果已经加载过了, 那就新起一个ClassLoader加载,
这也是为什么在tomcat中, 你上传了两个war包都执行的原因
private static class MyLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
File f = new File("E:\\workspace\\joys\\pay\\epay-service\\src\\main\\java" + name.replace(".", "/").concat(".class"));
if(!f.exists()) {
return super.loadClass(name);
}
try {
InputStream is = new FileInputStream(f);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}
}
public static void main(String[] args) throws Exception {
MyLoader m = new MyLoader();
Class clazz = m.loadClass("com.joy.epay.feign.Hello");
m = new MyLoader();
Class clazzNew = m.loadClass("ccom.joy.epay.feign.Hello");
System.out.println(clazz == clazzNew);
}
这个findClass是一个模板方法, 在ClassLoader中已经有了实现
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
下面是个例子, 并且验证了第二次是否会被加载
public class ClassLoaderTest extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("D:/test/", name.replace(".", "/").concat(".class"));
try {
FileInputStream fis = new FileInputStream(f);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
while ((b=fis.read()) !=0) {
baos.write(b);
}
byte[] bytes = baos.toByteArray();
baos.close();
fis.close();
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
//throws ClassNotFoundException
return super.findClass(name);
}
public static void main(String[] args) throws Exception {
ClassLoader l = new ClassLoaderTest();
// 加载Hello类
Class clazz = l.loadClass("com.joy.epay.feign.Hello");
Hello h = (Hello)clazz.newInstance();
h.print();
// 重复加载, 验证第二次是否还会加载
Class clazz1 = l.loadClass("com.joy.epay.feign.Hello");
System.out.println(clazz == clazz1);
// 打印该类的加载器
System.out.println(l.getClass().getClassLoader());
}
}
public class Hello {
void print(){
System.out.println("hello world!");
}
}