Java从编码到执行
在理解类加载前, 我觉得我们应该要先知道为什么我们写的代码可以被执行, 看下图:
我们写的代码编译成.class文件后, 被classloader加载到内存中, 同时也会把java自带的类库load到内存中
然后会编译器进行编译, 最后通过执行引擎执行
类加载的过程
先上个图:
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载(loading)
就是把一个.class文件load到内存中, load进去后在内存中创建了两块内容, 一块存储了这个.class文件的二进制内容, 二是创建了这个Class的对象, 并且将这个对象指向了第一块内容
验证(verification)
校验你这个.class文件内容是否合法
准备 (preparation)
会在这个阶段为你的静态变量赋初始值
解析(resolution)
在这个阶段将类、方法、属性等符号引用解析为直接引用(指针、偏移量等内存地址)
类加载器
类加载器分类以及加载范围
双亲委派
上图中当要加载某一个类时, 自下向上 检查该类是否已经加载, 又自上而下进行实际的查找和加载, 这就是双亲委派, 继续看图
当某个类加载器接收到类加载请求时, 会先去自己的缓存中监测有没有, 没有就交给自己的父加载器, 依次类推, 直到查找到返回为止;
如果BootStrap也没查找到, 就会进行加载, 当BootStrap发现这个类不归它管时, 则交给子加载器加载, 依次类推, 直到加载到返回为止;
如果都没加载上, 就抛出ClassNotFoundException以后看见这个异常就别怕了
举个例子:
java.lang.String类, 我们知道这个Java自带的String位于rt.jar中, 那它的加载将经过Custom、Application、ExtensionClassloader, String这个类都不归前面三个加载器负责, 最终由Bootstrap加载并返回;
为什么要搞双亲委派
最主要的原因是为了安全
其次是节约资源
双亲委派如何实现的
可以看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方法
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);
}
如何自定义一个类加载器
- 继承ClassLoader
- 重写findClass
- 自定义加载器逻辑
下面是个例子, 并且验证了第二次是否会被加载
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!");
}
}