JAVA类装载机制
类装载流程
JVM对类的装载包含以下五个步骤,当然除了对.class文件的装载,JVM也可以对满足类规范的二进制流进行装载。而只有被JVM装载完成的Class,才能在程序中被调用
加载
类加载的条件
一个类或接口,只有在被使用的时候,才会被装载。而类会初始化的场景如下:
- 创建对象时,也就是new对象或反射,反序列化,克隆
- 调用对象的静态方法或静态属性(final修饰的属性除外,final修饰的为常量,在JVM启动时,就已经被初始化在常量池中)
- 初始化子类,父类必须先与子类初始化
加载类
在加载类时,JVM必须完成以下工作
- 全类名获取二进制流,可以通过读入.class文件,或者jar包,甚至数据库中或HTTP传输的二进制流。
- 解析类的二进制流为方法区的数据结构
- 创建java.lang.Class类的实例,保存二进制流的元数据信息,比如类的方法,属性等等
验证
准备
准备阶段,也就是为这个类分配内存空间,并为常量设置一些初始值,如int初始为0
解析
解析也就是将类、接口、属性的符号引用,转为直接引用,也就是得到这个类属性、方法在内存中的偏移量。
初始化
初始化也就是生成类对应的clinit函数的指令。另外需要注意的是,类初始化的过程是加锁的,所以在多线程的环境下,需要注意类初始化产生相互等待,导致死锁的情况。
public class Demo1 {
static{
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Demo4();
}
}
public class Demo4 {
static{
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Demo1();
}
}
public class JVMTest {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
((ExecutorService) es).submit(()->{
new Demo1();
});
((ExecutorService) es).submit(()->{
new Demo4();
});
es.shutdown();
}
}
执行以上代码,我们会发现,程序一直处于阻塞状态。我们可以通过JDK自带的监控工具jvisualvm查看线程dump。linux系统可以通过jmap命令导出dump文件(jmap -dump:format=b,file=demodump.hprof pid)
认识ClassLoader
ClassLoader分类
类加载器包括启动类加载器、扩展类加载器、应用类加载器以及自定义加载器。启动类主要负责加载系统的核心类,如JDK中rt.jar中的类。扩展类加载器主要负责加载JDK中ext目录下的类。而应用类加载器一般就是用户的程序类。
ClassLoader的双亲委派模式
JVM在加载类时,会默认使用双亲委派模式,也就是会先判断当前类是否被加载,如果没有,则会委派给双亲加载,双亲加载失败后,则会自己加载。
双亲委派模式的优劣
双亲加载模式是单向的,加载层次结构清晰,职责明确。
但是也会带来一个问题,也就是顶层的类加载器假的类是无法访问底层类加载器加载的类。而顶层的类加载器加载的类,需要访问底层类加载器加载的类,这种场景是存在的。比如JDBC,XML解析,他们是在系统类中提供了一个接口,与工厂方法获取实例,但是接口和工程方法都是系统类加载器加载的,而具体的实现却是应用类加载器加载的。
双亲委派模式的补充
JDK的Thread类,提供了一个getContextClassLoader(),和setContextClassLoader(ClassLoader cl)方法,用于获取当期线程的上下文类加载器,对于当期线程来说,是可以全局获取的,也就是启动类加载器的类中,执行
ClassLoader cl = Thread.currentThread().getContextClassLoader();
那么就可以获取当期线程的传入类加载器,比如应用类加载器。这样就可以成功获取底层类加载器,加载相应的类了。
打破双亲委派模式
双亲委派模式,是通过ClassLoader的loadClass方法实现的,因此我们可以通过实现ClassLoader重写loadClass方法,来打破双亲委派模式。Tomcat就是这样实现的。
这样做有什么作用呢?
我们可以通过自定义类加载器,实现热替换功能,比如JSP,就是通过这种方式实现的。JSP其实就是JAVA代码,我们变更JSP文件后,那么就会生成新的.class类。我们可以自定义每个JSP对应一个类加载器,这样在变更JSP后,就会有新的类加载器去加载,而旧的类加载器直接废弃,从而实现热替换功能。