面试题
类加载机制,谈到双亲委派模型后会问到哪些违反了双亲委派模型?为什么要双亲委派?
简答
违反双亲委派模型
-
参考链接
https://blog.csdn.net/Dome_/article/details/99714356
https://www.zhihu.com/question/49667892
-
DriverManager 的初始化方法loadInitialDrivers
这就好像Application ClassLoader加载了本来应该由BootstrapClassLoader加载的java.sql.Connection一样。看起来像是违反了双亲委派模型。但实际上,这里的Connection的类型实际上是"com.mysql.jdbc.JDBC4Connection",也是个第三方类。AppClassLoader加载一个第三方类看起来并没有违反模型。
SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上已经打破了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。
双亲委派好处
-
保证优先级的层次关系,避免重复加载
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系,避免重复加载。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行
-
防止Java核心api中定义类型被随意替换
假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
JVM体系
- 参考链接
1. https://zhuanlan.zhihu.com/p/34426768
2. https://www.cnblogs.com/sunfie/p/5125283.html
3. https://www.cnblogs.com/lfs2640666960/p/9297176.html
类的加载机制
参考链接
http://www.importnew.com/18548.html
https://blog.csdn.net/u013256816/article/details/50837863
定义
-
概念
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在"运行时数据区"的方法区内(JDK1.8中是元数据区),然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
-
产物
类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
生命周期
- 例图
类加载过程
-
加载(Loading)
-
通过类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。获取方式为jar包、war包、网络中获取、JSP文件生成
-
将这个类字节流代表的静态存储结构转为方法区的运行时数据结构。这里只是转化数据结构,并未合并数据。(JDK1.7中方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)
-
在内存中生成一个代表此类的java.lang.Class对象,作为访问方法区中这这个类的各种数据的入口。(这个Class对象并没有规定是在Java堆内存中,JDK1.7中存放在方法区中,JDK1.8中存放在堆中)
java.lang.Class 对象和 static 成员变量在运行时内存的位置。这里先给出结论,JDK 1.8 中,两者都位于堆(Heap),且static 成员变量位于 Class对象内
JDK1.7中,加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class的对象(并没有明确规定是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面)
-
-
连接(Linking)
连接又包含三块内容:验证、准备、初始化。
-
验证(Verification),文件格式、元数据、字节码、符号引用验证;
-
准备(Preparation),为类的静态变量分配内存,并将其初始化为默认值;
(1) 类静态变量
为类的静态变量(static filed)在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100,静态变量a就会在准备阶段被赋默认值0。
(2) 成员变量
对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
(3) 静态常量
静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。
-
解析(Resolution),把类中的符号引用转换为直接引用
(1) 符号引用
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
(2) 直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在
(3) 应用举例
在类的加载过程中的解析阶段,Java虚拟机会把类的二进制数据中的符号引用 替换为 直接引用,如Worker类中一个方法:
public void gotoWork(){ car.run(); //这段代码在Worker类中的二进制表示为符号引用 }
在Worker类的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名 和 相关描述符组成。在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法在方法区的内存位置,这个指针就是直接引用。
(4) 解析分类
类或接口的解析
字段解析
类方法解析
接口方法解析
-
-
初始化(Initialization)
类的初始化的主要工作是为类的静态变量赋予正确的初始值
-
主动引用
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻"初始化"(加载,验证,准备,自然需要在此之前开始)(1) 使用new字节码指令创建类的实例,或者使用getStatic、putStatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行初始化
(2) 通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化
(3) 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化
(4) 当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类
(5) 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化
-
被动引用
除了以上五种情况,所有其他的类引用方式都不会触发类初始化,称为"被动引用"
(1) 通过子类引用父类的静态字段,对于父类属于"主动引用"的第一种情况,对于子类,没有符合"主动引用"的情况,故子类不会进行初始化
//父类 public class SuperClass { //静态变量value public static int value = 666; //静态块,父类初始化时会调用 static{ System.out.println("父类初始化!"); } } //子类 public class SubClass extends SuperClass{ //静态块,子类初始化时会调用 static{ System.out.println("子类初始化!"); } } //主类、测试类 public class NotInit { public static void main(String[] args){ System.out.println(SubClass.value); } } //输出结果 父类初始化! 666
(2) 通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何"主动引用"条款,属于"被动引用"
//父类 public class SuperClass { //静态变量value public static int value = 666; //静态块,父类初始化时会调用 static{ System.out.println("父类初始化!"); } } //主类、测试类 public class NotInit { public static void main(String[] args){ SuperClass[] test = new SuperClass[10]; } } //输出结果 无任何输出结果
(3) 静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类,这是一个特例,需要特别记忆,不会触发类的初始化
//常量类 public class ConstClass { static{ System.out.println("常量类初始化!"); } public static final String HELLOWORLD = "hello world!"; } //主类、测试类 public class NotInit { public static void main(String[] args){ System.out.println(ConstClass.HELLOWORLD); } } //输出结果 hello world!
-
-
其他
-
使用(Using)
new出对象程序中使用
-
卸载(Unloading)
执行垃圾回收
-
类加载器
-
类加载器方式
-
作用
类加载器的作用不仅仅是实现类的加载,它还与类的的"相等"判定有关,关系着Java"相等"判定方法的返回结果,只有在满足如下三个类"相等"判定条件,才能判定两个类相等:
-
两个类来自于同一个Class文件
-
两个类是由同一个虚拟机加载
-
两个类是由同一个类加载器加载
-
-
Java常用判定"相等"相关方法
-
判断两个实例对象的引用是否指向内存中同一个实例对象,使用 Class对象的equals()方法,obj1.equals(obj2)
-
判断实例对象是否为某个类、接口或其子类、子接口的实例对象,使用Class对象的isInstance()方法,class.isInstance(obj)
-
判断实例对象是否为某个类、接口的实例,使用instanceof关键字,obj instanceof class
-
判断一个类是否为另一个类本身或其子类、子接口,可以使用Class对象的isAssignableFrom()方法,class1.isAssignableFrom(class2)
-
-
常用类加载器
- 启动类加载器(Bootstrap ClassLoader)
负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库。根类加载器非常特殊,它不是java.lang.ClassLoader的子类,它是JVM自身内部由C/C++实现的,并不是Java实现的
- 扩展类加载器(Extension ClassLoader)
该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 系统类(应用程序类)加载器(Application ClassLoader)
该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
类加载机制
-
全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
-
双亲委派(父类委托)
让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
-
缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
类加载器关系
-
简介
以组合关系复用父类加载器的父子关系,注意,这里的父子关系并不是以继承关系实现的
-
案例代码
//验证类加载器与类加载器间的父子关系 public static void main(String[] args) throws Exception{ //获取系统/应用类加载器 ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); System.out.println("系统/应用类加载器:" + appClassLoader); //获取系统/应用类加载器的父类加载器,得到扩展类加载器 ClassLoader extcClassLoader = appClassLoader.getParent(); System.out.println("扩展类加载器" + extcClassLoader); System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs")); //获取扩展类加载器的父加载器,但因根类加载器并不是用Java实现的所以不能获取 System.out.println("扩展类的父类加载器:" + extcClassLoader.getParent()); } //输出结果 系统/应用类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2 扩展类加载器sun.misc.Launcher$ExtClassLoader@254989ff 扩展类加载器的加载路径:D:\Develop\java\jdk8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext 扩展类的父类加载器:null
双亲委派模型源码
-
源码分析
主要体现在ClassLoader的loadClass()方法中,思路很简单:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,调用自己的findClass()方法进行加载。
-
案例源码
public abstract class ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ 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(); } } if (resolve) { resolveClass(c); } return c; } } }
-
验证代码
public class ClassLoaderTest { public static void main(String[] args){ //输出ClassLoaderText的类加载器名称 System.out.println("ClassLoaderText类的加载器的名称:"+ClassLoaderTest.class.getClassLoader().getClass().getName()); System.out.println("System类的加载器的名称:"+System.class.getClassLoader()); System.out.println("List类的加载器的名称:"+List.class.getClassLoader()); ClassLoader cl = ClassLoaderTest.class.getClassLoader(); while(cl != null){ System.out.print(cl.getClass().getName()+"->"); cl = cl.getParent(); } System.out.println(cl); } //输出结果 ClassLoaderText类的加载器的名称:sun.misc.Launcher$AppClassLoader System类的加载器的名称:null List类的加载器的名称:null sun.misc.Launcher$AppClassLoader->sun.misc.Launcher$ExtClassLoader->null
-
流程分析
-
ClassLoaderTest类是用户定义的类,位于CLASSPATH下,由系统/应用程序类加载器加载。
-
System类与List类都属于Java核心类,由祖先类启动类加载器加载,而启动类加载器是在JVM内部通过C/C++实现的,并不是Java,自然也就不能继承ClassLoader类,自然就不能输出其名称。
-
而箭头项代表的就是类加载的流程,层级委托,从祖先类加载器开始,直到系统/应用程序类加载器处才被加载。
-
-
其他
把类打成jar包,拷贝入%JAVA_HOME%/jre/lib/ext目录下,再次运行ClassLoaderTest类
ClassLoaderText类的加载器的名称:sun.misc.Launcher$ExtClassLoader
System类的加载器的名称:null
List类的加载器的名称:null
sun.misc.Launcher$ExtClassLoader->null
因为类的Jar包放到了ExtClassLoader的加载目录下,所以在根目录找不到相应类后,在ExtClassLoader处就完成了类加载,而忽略了APPClassLoader阶段
其他
-
Java中Class对象详解
https://blog.csdn.net/mcryeasy/article/details/52344729
-
类加载过程
https://blog.csdn.net/zhangliangzi/article/details/51319033
-
类加载机制与双亲委派模型
https://blog.csdn.net/zhangliangzi/article/details/51338291