![](https://img.haomeiwen.com/i2043702/6622ab6930297858.jpg)
类加载基础概念
尝试用5W1H模型来聊聊Java的类加载。
什么是类加载? 简单的说,把字节码加载到JVM中的过程,我们就称之为类加载。输入是某个类的.class文件的字节流,输出是JVM所管理的方法区中关于该类的信息。
为什么要有类加载? 我的理解是为了更好的支持动态特性,比如说热部署,就是利用了JVM可以动态加载字节码的机制实现的。
什么时候进行类加载? 总的来说,JVM需要某个类的信息,而又没有的时候,就会触发类加载。具体来说分了以下几个场景:
- 遇到new、getstatic、putstatic、invokestatic等指令时,如果类还没有加载过就会触发类加载;
- 子类进行类加载时,如果父类还没有加载过,会先触发父类的加载;
- 使用反射进行各种操作时,如果类还没有加载过,会先进行类加载。
- 虚拟机启动时,会首先加载含有main方法的类。
- 其他情况,这里不是抄书,所以我们先不再枚举。
谁来负责类加载? 类加载有专门的类加载器来完成,类加载器又有等级森严的层级关系,爷爷辈的类加载器叫启动类加载器,然后是爸爸辈,叫拓展类加载器,最后是应用程序类加载器。这里涉及到一个类加载过程中各个类加载器是如何分工合作的,会在双亲委派模型中提到。
怎样进行类加载? 前面提到过类加载就是把类的字节码塞进虚拟机的过程,那么具体怎么做呢?
首先,类加载器需要从某处获得字节码的二进制字节流。为什么不说字节码文件?因为除了从.class文件中获取,还可以从压缩包中解压获取,从网络中获取(比如Applet),甚至是动态生成一个(想想动态代理)都是可以的。这个动作,我们称为加载。(TO-DO 这个时候生成Class对象了吗?)
接着,这个对象还不能直接使用,我们需要把针对它做各种校验,比如字节码本身是否合规,是否是该版本的虚拟机支持,如果都通过了,就需要给静态变量开辟一块内存区域,然后赋零值,这里的零值指的是,当内存中没有数据时,变量的值,比如对于int型来说零值是0,对于boolean型来说,零值是false。最后,如果这个类中存在符号引用,还需要把符号引用解析为具体的内存地址。以上所有的动作,我们合并起来,称之为链接。
最后,终于到了给静态字段赋值的时候了,无论是直接赋值还是通过静态块来完成,编译器都会把这些赋值语句收集到一起,并且按程序书写的顺序,然后放在一个叫clinit
的方法中,依次执行。这个动作,我们称为初始化。
类加载进阶
以上是针对类加载机制的一个简单介绍,下面我们进行一些更加高阶的讲解。
一种优雅的单例实现
实现单例有多种不同的写法,也个有优缺点,其中“饿汉式”的写法最为简洁,但缺点是一旦触发了类加载就会同步进行实例化。触发的机制前面的基础篇中已经提到过,比如调用Resource中存在的任意一个static字段或者方法,就会触发Resource类的类加载。
而下面的写法通过引入静态内部类完美的解决了这个问题。结合刚才提到的类加载机制,说说这是为什么?
public class Resource {
private Resource() {}
public static Resource getInstance() {
return Holder.resource;
}
private static class Holder {
public static final Resource resource = new Resource();
}
}
关于双亲委派模型
刚才介绍了几个不同的类加载器,那么他们之间是怎样合作的?我们结合ClassLoader类中的loadClass方法来看:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检查该类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
// 针对未加载过的类,先尝试让父类加载器进行加载
try {
// 启动类加载器是通过C++实现的,只能表示为null
// 因此这里有2个逻辑分支
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 返回一个启动类加载器加载的类,如果没有则返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 如果父类加载器无法加载,再尝试自己加载
if (c == null) {
c = findClass(name);
}
}
// 其他实现细节...
return c;
}
}
通过自定义类加载器实现热部署
热部署指的是不需要重启应用就可以动态的替换掉其中的一些功能,类加载器给我们提供了这样一种实现的思路。
首先,我们说在Java的世界里,通过一个类的全限定名 + 类加载器,可以唯一的定位一个类,也就是说,哪怕是同一个.class文件,通过不同的类加载器进入JVM,它们之间也是互相隔离的。
基于上述事实,当我们希望只是升级某个类的功能时,就可以通过这样的机制来实现:为该类实例化一个新的类加载器,并重新加载该类,最后替换掉之前旧的版本。
根据这样的思路,我们可以定义一个MyTest类,拥有一个showVersion()方法,在第一个版本中会打印1.0,在第二个版本中会打印2.0,代表功能进行了升级。
public class MyTest {
public void showVersion() {
System.out.println("1.0版本");
}
}
接着,需要自定义一个类加载器,重写部分方法,简单来说,它会根据类的全限定名,在/tmp目录下找对应的字节码文件,针对特定的类,如MyTest,不经过双亲委派模型,直接加载进内存中。
public class MyClassLoader extends ClassLoader {
// 指定那些类可以通过自定义类加载器的方式加载
private Set<String> classNamesLoadMyself = new HashSet<>();
public MyClassLoader(String ... classNames) {
for (String className : classNames) {
classNamesLoadMyself.add(className);
}
}
@Override
protected Class<?> findClass(String name) {
// 根据路径和类名找到对应的文件并转化为相应的字节流
byte[] bytes = FileUtil.getClassByte("/tmp", name);
return defineClass(name, bytes, 0, bytes.length);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 如果是指定了要自定义类加载的类,则绕开双亲委派模型
if (classNamesLoadMyself.contains(name)) {
return findClass(name);
}
return super.loadClass(name);
}
}
最后,我们对这个自定义的类加载器做一个测试。
public class MyClassLoaderClient {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
// 实例化一个类加载器
MyClassLoader myClassLoader = new MyClassLoader("MyTest");
// 注意这里不能直接强制类型转化为MyTest
Object myTest = myClassLoader.loadClass(className).newInstance();
myTest.getClass().getMethod("showVersion").invoke(myTest);
// 休眠1秒
TimeUnit.SECONDS.sleep(1);
}
}
}
在这个测试类中有一行注释,不能将实例化的MyTest做强制类型转换,请问这是为什么呢?
网友评论