类加载器的关系
类加载器的分类
- JVM支持两种类加载器,一种为引导类加载器(Bootstrap ClassLoader),另外一种是自定义类加载器(User Defined ClassLoader)
- 引导类加载器是由C/C++编写的无法访问到
- Java虚拟机规定:所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
- 最常见的类加载器只有三个(如上图所示)
用户自定义的类会被系统类加载器所加载,核心类库的类会被引导类加载器所加载
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
系统自带的类加载器介绍
-
启动类加载器(引导类加载器、Bootstrap ClassLoader)
- 由c/c++语言实现的,嵌套在jvm内部
- 用来加载java核心库
- 并不继承java.lang.ClassLoader,没有父加载器
- 为扩展类加载器和系统类加载器的父加载器
- 只能加载java、javax、sun开头的类
-
扩展类加载器(Extension ClassLoader)
- java语言编写,sun.misc.Launche包下。
- 派生于ClassLoader类,父类加载器为Bootstrap ClassLoader
- 从java.ext.dirs系统属性指定的目录中加载类库或者加载jre/lib/ext子目录下的类库(用户可以在该目录下编写JAR,也会由此加载器所加载)
-
系统类加载器(System ClassLoaderAppClassLoader)
- 派生于ClassLoader,父类加载器为Extension ClassLoader
- 负责加载classpath或者系统属性java.class.path指定路径下的类库
- java语言编写,sun.misc.Launche包下。
- 负责加载程序中默认的类,可以通过getSystemClassLoader()方法获取该类的加载器。
-
用户自定义类加载器(后面详细介绍)
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏(可以对字节码文件加密)
- 继承ClassLoader类方式实现自定义类加载器
关于ClassLoader
-
ClassLoader是一个抽象类,其后的所有类加载器都继承此类
注:这些方法都不是抽象方法。
获取ClassLoader的路径
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
双亲委派机制(面试)
-
Java虚拟机对class文件采用的是按需加载的方式,当需要使用到这个类的时候才会对它的class文件加载到内存生成class对象,加载的过程中使用的双亲委派模式,即把请求交给父类处理。
-
如果一个类加载器收到了类加载的请求,它不会自己加载,而是先把这个请求给自己的父类加载器去执行
-
如果这个父类加载器还有父类加载器,则会再将请求给自己的父类加载器,依次递归到顶层的启动类加载器
-
依次进行判断是否能完成委派(加载此类),若能完成委派则该类就由此加载器加载,若无法完成委派,则将委托给子类加载器进行判断是否能完成委派,依次递归到底层加载器,若期间被加载则完成加载阶段不会再递归(注)。
注:类只能被一个加载器所加载。
-
双亲委派的优势
-
避免类的重复加载
-
保护程序的安全,防止核心API被篡改
-
例如:
-
创建一个java.lang.String类,因为有双亲委派的机制,所以会将String类交给引导类加载器来判断是否能被加载。引导加载器判断可以加载此类(是核心类中的String),完成加载,则"恶意"写的String类无法生效,防止String类被恶意篡改。这里也称沙箱安全机制(保护java核心源代码)
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
//因为加载的是核心类的String,在String中找不到main方法
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();//无输出
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
package java.lang;
public class ShkStart {
//错误:java.lang.SecurityException: Prohibited package name: java.lang
public static void main(String[] args) {
System.out.println("hello!");
}
}
//因为java.lang包由引导类加载器加载,引导类中并没有此类,为了安全引导类
破坏双亲委派模型:
较大规模的破坏双亲委派模型的有3种:
-
由于双亲委派模型是在JDK1.2之后才引入的,所以在JDK1.2之前是不符合双亲委派模型的:
-
ClassLoader这类在JDK1.0开始有存在的,在JDK1.2之前,ClassLoader中是通过私有方法loadClassInternal()去调用自己内部的loadClass()。为了满足双亲委派以及向下兼容,在JDK1.2后的ClassLoader类中,又为该类添加了protected的findClass()方法,JDK1.2之后就不推荐通过覆盖重写loadClass()方法了,而是在新添加的findClass()方法中书写自己的类加载逻辑,若loadClass()方法中的父类加载失败则会调用自己的findClass()方法。
-
由于双亲委派模型的旨意是越核心的类越由高层的加载器所加载(上文提到过的String类),倘若这些核心类要去调用用户的基础类,例如JNDI服务(是对 资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能"认识"这些代码)
-
为了解决调用问题,设计了一个线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
-
有了这个上下文类加载器,就可以去加载所需要的SPI代码,实际上就是从父类加载器去请求子类加载器去完成类的加载驱动,违背了双亲委派的一般性规则。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
-
为了满足"热部署"、"动态部署"等功能而导致的。在OSGi(动态模块技术)环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照顺序进行类搜索
关于类加载器的一些补充
1. JVM中判断一个类是否是同一个类有两个必要条件:
- 这两个类的全限定名要一致
- 这两个类被同一个类加载器加载。
2. 对类加载器的引用:
- JVM必须知道一个类型是由引导类加载器(启动类加载器)加载的还是由用户类加载器加载的。
- 如果一个类是由用户类加载器所加载的,那么JVM会将这个类加载器的一个引用作为类信息的一部分保存在方法区。
- 当解析一个类型到另外一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
3. 类的主动使用和被动使用
- 主动使用:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
* 初始化一个类的子类
* JVM启动时被标明为启动类的类 - JDK 7 开始提供的动态代理:java.invoke.MethodHandle实例的解析结果,REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化、则初始化
* 除以上7种情况,其他使用Java类的方式都为被动使用,被动使用不会导致类的初始化。
最后
大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料放在里面,助你圆梦BAT!文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!