一篇文章读懂Java类加载器
Java类加载器算是一个老生常谈的问题,大多Java工程师也都对其中的知识点倒背如流,最近在看源码的时候发现有一些细节的地方理解还是比较模糊,正好写一篇文章梳理一下。
关于Java类加载器的知识,网上一搜一大片,我自己也看过很多文档,博客。资料虽然很多,但还是希望通过本文尽量写出一些自己的理解,自己的东西。如果只是重复别人写的内容那就失去写作的意义了。
类加载器结构
名称解释:
- 根类加载器,也叫引导类加载器、启动类加载器。由于它不属于Java类库,这里就不说它对应的类名了,很多人喜欢称BootstrapClassLoader。本文都称之为根类加载器。
加载路径:<JAVA_HOME>\lib - 扩展类加载器,对应Java类名为ExtClassLoader,该类是sun.misc.Launcher的一个内部类。
加载路径:<JAVA_HOME>\lib\ext - 应用类加载器,对应Java类名为AppClassLoader,该类是sun.misc.Launcher的一个内部类。
加载路径:用户目录
//可以通过这种方式打印加载路径
System.out.println("boot:"+System.getProperty("sun.boot.class.path"));
System.out.println("ext:"+System.getProperty("java.ext.dirs"));
System.out.println("app:"+System.getProperty("java.class.path"));
重点说明:
- 根类加载器对于普通Java工程师来讲可以理解成一个概念上的东西,因为我们无法通过Java代码获取到根类加载器,它属于JVM层面。
- 除了根类加载器之外,其他两个扩展类加载器和应用类加载器都是通过类sun.misc.Launcher进行初始化,而Launcher类则由根类加载器进行加载。
看下Launcher初始化源码:
public Launcher() { Launcher.ExtClassLoader var1; try { //初始化扩展类加载器,注意这里构造函数没有入参,即无法获取根类加载器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { //初始化应用类加载器,注意这里的入参就是扩展类加载器 this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } //设置上下文类加载器,这个后面会详细说 Thread.currentThread().setContextClassLoader(this.loader); //删除了一些安全方面的代码 //... }
双亲委派模型
双亲委派模型是指当我们调用类加载器的loadClass方法进行类加载时,该类加载器会首先请求它的父类加载器进行加载,依次递归。如果所有父类加载器都加载失败,则当前类加载器自己进行加载操作。
逻辑很简单,通过ClassLoader类的源码来分析一下。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//进行类加载操作时首先要加锁,避免并发加载
synchronized (getClassLoadingLock(name)) {
//首先判断指定类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果当前类没有被加载且父类加载器不为null,则请求父类加载器进行加载操作
c = parent.loadClass(name, false);
} else {
//如果当前类没有被加载且父类加载器为null,则请求根类加载器进行加载操作
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//如果父类加载器加载失败,则由当前类加载器进行加载,
c = findClass(name);
//进行一些统计操作
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//初始化该类
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型的实现逻辑总体看还是非常简单明了的。
这里有几个细节需要说明:
- ClassLoader类是一个抽象类,但却没有包含任何抽象方法。
- 如果要实现自己的类加载器且不破坏双亲委派模型,只需要继承ClassLoader类并重写findClass方法。
- 如果要实现自己的类加载器且破坏双亲委派模型,则需要继承ClassLoader类并重写loadClass,findClass方法。
令人疑惑的系统类加载器
当你把上面的知识都搞清楚以后,会发现ClassLoader类中有个方法是getSystemClassLoader,系统类加载器,这又是什么?
系统类加载器是个容易让人混淆的概念,我一度以为它就是应用类加载器的别名,就跟启动类加载器和根类加载器道理一样。事实上,默认情况下我们通过ClassLoader.getSystemClassLoader()获取到的系统类加载器也确实是应用类加载器。
很多资料在说类加载器结构的时候会直接把应用类加载器说成是系统类加载器,其实我们通过类名就可以判断两个不是一回事。
系统类加载器可以通过System.setProperty("java.system.class.loader", xxx类名)进行自定义设置。
系统类加载器不是一个全新的加载器,它只是一个概念,本质上还是上述说的四大类加载器(把用户自定义类加载器算进去),至于提出这个概念的原因以及使用场景,还需要继续考究。
被人忽略的上下文类加载器
上面讨论了各个类加载器的加载路径。鉴于双亲委派模型的设计,子类加载器都保留了父类加载器的引用,也就是说当由子类加载器加载的类需要访问由父类加载器加载的类时,毫无疑问是可以访问到的。但考虑一种场景,会不会有父类加载器加载的类需要访问子类加载器加载的类这种情况?如果有,怎么解决(父类加载器并没有子类加载器的引用)?
这就是我们要讨论的常常被人们忽略的上下文类加载器。
经典案例:
JDBC是Java制定的一套访问数据库的标准接口,它包含在Java基础类库中,也就是说它是由根类加载器加载的。与此同时,各个数据库厂商会各自实现这套接口来让Java工程师可以访问自己的数据库,而这部分实现类库是需要Java工程师在工程中作为一个第三方依赖引入使用的,也就是说这部分实现类库是由应用类加载器进行加载的。
先上一段Java获取Mysql连接的代码:
//加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//连接数据库
Connection conn = DriverManager.getConnection(url, user, password);
这里DriverManager类就属于Java基础类库,由根类加载器加载。我们可以通过它获取到数据库的连接,显然是它通过com.mysql.jdbc.Driver驱动成功连接到了数据库,上面也说了数据库驱动(作为第三方类库引入)是由应用类加载器加载的。这个场景就是典型的由父类加载器加载的类需要访问由子类加载器加载的类。
Java是怎么实现这种逆向访问的呢?直接看DriverManager类的源码:
//建立数据库连接各个不同参数的方法最终都会走到这里
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//获取调用者的类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
//如果为null,则使用上下文类加载器
//这里是重点,什么时候类加载器才会为null? 当然就是由根类加载器加载的类了
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
//...省略
for(DriverInfo aDriver : registeredDrivers) {
//使用上下文类加载器去加载驱动
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
//如果加载成功,则进行连接
Connection con = aDriver.driver.connect(url, info);
//...
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
//...
}
}
重点说明:
为什么上下文类加载器就可以加载到数据库驱动呢?回到上面一开始Launcher初始化类加载器的源码,我们发现原来所谓的上下文类加载器本质上就是应用类加载器,有没有豁然开朗的感觉?上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并不是一个全新的类加载器,它本质上就是应用类加载器。
基本上我理解的Java类加载器就这么多知识,如果有没提到的或者是错误的地方,欢迎交流。
Java学习交流QQ群:523047986 禁止闲聊,非喜勿进!
一篇文章读懂Java类加载器的更多相关文章
- 一篇文章弄懂 Java 反射的使用
说到Java反射,必须先把 Java 的字节码搞明白了,也就是 Class , 大 Class 在之前的文章中,我们知道了Java的大Class就是类的字节码,就是一个普通的类,里面保存的是类的信息, ...
- 一篇文章读懂JSON
什么是json? W3C JSON定义修改版: JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation) JSON 是轻量级的文本数据交换格式,并不是 ...
- 一文读懂Java类加载机制
Java 类加载机制 Java 类加载机制详解. @pdai Java 类加载机制 类的生命周期 类的加载:查找并加载类的二进制数据 连接 验证:确保被加载的类的正确性 准备:为类的静态变量分配内存, ...
- Java多线程详解——一篇文章搞懂Java多线程
目录 1. 基本概念 2. 线程的创建和启动 2.1. 多线程实现的原理 2.2.多线程的创建,方式一:继承于Thread类 2.3.多线程的创建,方式一:创建Thread匿名子类(也属于方法一) 2 ...
- Java枚举类与注解详解——一篇文章读懂枚举类与注解详
目录 一.枚举类 ① 自定义枚举类 ② enum关键字定义枚举类 ③ enum 枚举类的方法 ④ enum 枚举类实现接口 二.注解 ① 生成文档相关注解 ②注解在编译时进行格式检查 ③注解跟踪代码的 ...
- 一篇文章搞懂G1收集器
一.何为G1收集器 The Garbage-First (G1) garbage collector is a server-style garbage collector, targeted for ...
- 一篇文章看懂Java并发和线程安全
一.前言 长久以来,一直想剖析一下Java线程安全的本质,但是苦于有些微观的点想不明白,便搁置了下来,前段时间慢慢想明白了,便把所有的点串联起来,趁着思路清晰,整理成这样一篇文章. 二.导读 1.为什 ...
- 一篇文章看懂java反射机制(反射实例化对象-反射获得构造方法,获得普通方法,获得字段属性)
Class<?> cls = Class.forName("cn.mldn.demo.Person"); // 取得Class对象传入一个包名+类名的字符串就可以得到C ...
- 一篇文章读懂开源web引擎Crosswalk-《转载》
前言 Web技术的优势早已被广大应用开发者熟知,比如可与云服务轻松集成,基于响应式UI设计的精美布局,高度的开放性,跨平台能力, 高效的分发与部署等等.伴随着移动互联网的快速发展与HTML5技术的逐步 ...
随机推荐
- 【MYSQL】解决Mysql直接登录问题(删除匿名用户)(转)
刚安装的Mysql会存在匿名用户. 在命令行下输入mysql,(如果这时提示不是外部或内部指令,那就把mysql server文件下的bin目录添加到系统路径Path中) 如果没有任何提示,直接进入& ...
- 终结 finalize()和垃圾回收(garbage collection)
1.为什么要有finalize()方法? 假定你的对象(并非使用new)获得了一块"特殊"的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以他不知道该如何释放该对象 ...
- Scrapy爬取西刺代理ip流程
西刺代理爬虫 1. 新建项目和爬虫 scrapy startproject daili_ips ...... cd daili_ips/ #爬虫名称和domains scrapy genspider ...
- Android中的服务
Android中的服务 四大组件都是运行在主线程 Android中的服务,是在后台运行 .可以理解成是在后台运行并且是没有界面的Activity. Foreground process 前台进程 ,用 ...
- 虚拟机下克隆3个centos系统并配置IP访问网络(转载)
此文是保证linux系统能够上网 先查看本机的IP 打开虚拟机,更改虚拟机为桥接方式 在第一个虚拟机上打开终端,在命令行输入setup,选择NETWORK CONFIGRATION 回车后, 配置IP ...
- Java虚拟机垃圾回收机制
在Java虚拟机中,对象和数组的内存都是在堆中分配的,垃圾收集器主要回收的内存就是再堆内存中.如果在Java程序运行过程中,动态创建的对象或者数组没有及时得到回收,持续积累,最终堆内存就会被占满,导致 ...
- 敏捷开发之产品日日新,一步通之---自动化代码构建->自动化打包->自动化安装部署
本文将介绍如何自动化实现代码构建,自动化代码打包成exe安装包,自动化安装到测试环境.通过计划任务的方式,每天自动化发布最新的产品供老板展示,供测试人员使用,真正实现敏捷的快速迭代. 自动代码构建 自 ...
- 【模板】51Nod--1085 01背包
在N件物品取出若干件放在容量为W的背包里,每件物品的体积为W1,W2--Wn(Wi为整数),与之相对应的价值为P1,P2--Pn(Pi为整数).求背包能够容纳的最大价值. Input 第1行,2个整数 ...
- DynamicXml
/* var xml = @"<root><books><book is_read=""false""><a ...
- Luogu P1451 求细胞数量
题目描述 一矩形阵列由数字0到9组成,数字1到9代表细胞,细胞的定义为沿细胞数字上下左右若还是细胞数字则为同一细胞,求给定矩形阵列的细胞个数.(1<=m,n<=100)? 输入输出格式 输 ...