在了解类加载的双亲委派模型之前,先了解一下以下前置知识:
1. Java运行时一个类是什么时候被加载的?
以HotSpot 虚拟机是按需加载,在需要用到该类的时候加载这个类,那什么时候用到该类呢?用到该类,无外乎出现在创建类的对象、访问类的静态变量和方法、反射获取类对象等情况。
2. JVM一个类的加载过程?
一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:
- 加载(Loading)
从classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间,此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载; - 验证(Verification)
验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全; - 准备(Preparation)
类变量赋默认初始值,int为0, long为0L, boolean为false,引用类型为null;常量赋正式值; - 解析(Resolution)
把符号引用翻译为直接引用 - 初始化(Initialization)
当我们new一个类的对象、访问一个类的静态属性、修改一个类的静态属性、调用一个类的静态方法、用反射API对一个类进行调用、初始化当前类,其父类也会被初始化…那么这些都会触发类的初始化; - 使用(Using)
使用这个类 - 卸载(Unloading)
出现以下情况时,会卸载该类:
(1)该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
(2)加载该类的ClassLoader 已经被GC;
(3)该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类时。
其中验证、准备、解析三个阶段统称为连接(Linking) :
3. 一个类被初始化的过程?
类的初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码;
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,才真正初始化类变量和其他资源;
下面一代码演示:
public class Test01 {
//静态常量==准备阶段
public static final String staticConstantField = "静态常量";
//静态变量==准备阶段赋值为null,初始化阶段赋值为静态变量
public static String staticField = "静态变量";
//变量==创建对象的时候赋值
public String field = "变量";
//静态初始化块==初始化阶段执行static
static {
System.out.println(staticConstantField);
System.out.println(staticField);
System.out.println("静态初始化块");
}
//初始化块==创建对象的时候执行
{
System.out.println(field);
System.out.println("初始化块");
}
//构造器==创建对象的时候执行
public Test01() {
System.out.println("构造器");
}
public static void main(String[] args) {
new Test01();
}
}
执行结果:
4. 类加载器架构?
自JDK1.2开始,Java一直保持着三层类加载器架构:
三个类加载器各自的作用:
1. 启动类加载器(Bootstrap ClassLoader)
启动类加载器是根的类加载器),依靠c++实现,启动类加载器加载jdk中<JAVA_HOME>\jre\lib\目录下的部分jar包,如rt.jar、resources.jar、charsets.jar等,(并非该目录下所有jar包)还有就是加载被-Xbootclasspath参数所指定的路径中存放的类库;
2.扩展类加载器(Extension ClassLoader)
sun.misc.Launcher$ExtClassLoader,扩展类加载器主要加载jdk中<JAVA_HOME>\jre\lib\ext目录下的jar包以及被 java.ext.dirs系统变量所指定的路径中所有的类库;
3.应用程序类加载器(Application ClassLoader)
系统的类加载器sun.misc.Launcher$AppClassLoader,主要加载用户类路径(ClassPath)上所有的类库;
注意:此处的三层架构并不代表三个类加载器之间是继承关系,他们之间的关系如下,都是继承ClassLoader这一抽象类
5. JVM类加载的双亲委派模型(重点)
双亲委派模型的工作过程是:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,下一层类加载器才会尝试自己去加载;
以加载String类为例:
首先会尝试让AppClassLoader去加载,但是AppClassLoader自己不去加载,它会委派ExtClassLoader去加载,但是ExtClassLoader自己也不会加载,它会继续向上委派BootStrap ClassLoader去加载,于是,BootStrap ClassLoader就会在<JAVA_HOME>\jre\lib\目录下相应的jar包去查找,最后在rt.jar包中找到了String类,于是类加载结束。
如果一个类在BootStrap ClassLoader中未加载到,会继续向下(ExtClassLoader)尝试加载,若还是未加载到,则会继续向下到AppClassLoader中加载,若最后都未加载都,则会抛出ClassLoaderFoundException异常。
6. JDK为什么要设计双亲委派模型,有什么好处?
- 1、确保安全,避免Java核心类库被修改;
如jdk中,有java.lang.String这个类,我们再自己定义一个相同结构的类时,就会出错。 - 2、避免重复加载;
- 3、保证类的唯一性;
7. 可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?
双亲委派模型是可以打破的;
来观察一下双亲委派模型源码:
双亲委派模型的核心源码:
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
综上可得,如果想要打破JVM双亲委派模型,那么就自定义一个类加载器,继承classLoader类,重写其中的loadClass方法,使其不进行双亲委派即可。
后续博客会继续更新JVM内存模型噢!