JVM:Java 类的加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备和解析三个部分统称为连接。

加载:加载是类加载的第一个阶段,这个阶段,首先要根据类的全限定名来获取定义此类的二进制字节六,讲字节六转化为方法区运行时数据结构。在 Java 堆生成一个代表这个类的 java.lang.class 对象,作为方法区的访问入口。
验证:这一步的的目的是确保 class 文件的字节六包含的信息符合虚拟机的要求。
准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都会在方法区中进行分配。仅仅是类变量,不包括实例变量。
public static int value = 123;
变量在准备阶段过后的初始值为0而不是123,123的赋值要在变量初始化以后才会完成。
解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化:初始化是类加载的最后一步,这一步会根据程序员给定的值去初始化一些资源。
加载是我们使用一个类的第一步,加载是如何完成的那?
类加载器
虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到 Java 虚拟机外部去实现,以便让程序自己去决定如何获取所需要的类,这个动作的代码模块称为类加载器。
对于一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,比较两个类是否相等需要在这两个类是由同一个类加载器加载的前提下才有意义。
- 启动类加载器(Bootstrap ClassLoader):这个类负责将 \lib 目录中的类库加载到内存中,启动类加载器无法被 Java 程序直接引用。
- 扩展类加载器(Extension ClassLoader):负责加载 \lib\ext 目录中的类。开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器是 ClassLoader 中 getSystemClassLoader() 方法的返回值,所以一般称为系统类加载器。如果没有自定义过加载器,一般情况下这个就是默认的类加载器。
- 自定义类加载器(User ClassLoader):通过自定义类加载器可以实现一些动态加载的功能,比如 SPI。
双亲委派模型
JVM 在加载类时默认采用的是双亲委派模型机制。通俗地讲,某个特定的类加载器在接到加载类的请求时,首先讲加载任务委托给父类加载器,因此所有加载请求最总都应该传送到顶层的启动类加载器中。如果父类无法完成加载请求,子类才会尝试自己加载。
所以,越基础的类会由越上层的加载器加载。
如果不使用双亲委派模型,用户自己写一个 Object 类放入 ClassPath,那么系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证。现在你可以尝试自己写一个名为 Object 的类,可以被编译,但永远无法运行。因为最后加载时都会先委派给父类去加载,在 rt.jar 搜寻自身目录时就会找到系统定义的 Object 类,所以你定义的 Object 类永远无法被加载和运行。

双亲委派模型的好处是保证了核心类库不配覆盖和篡改。
打破双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器实现方式。Java 世界中大部分的类加载器都遵循这个模型。
双亲委派模型第一次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的 API。那如果基础类又要调用回用户的代码,怎么办?
比如 JNDI 服务,JNDI 现在是 Java 的标准服务,他的代码由启动类加载器去加载(rt.jar),但 JNDI 需要由独立厂商实现并部署在应用程序的 Class Path 下的 JNDI 接口提供者的代码,启动类加载器不可能认识这些代码,因为启动类加载器的搜索范围找不到用户应用程序类。
为了解决这个问题,Java 团队设计了一个不太优雅的设计:线程上下文加载器这个类加载器可以通过java.lang.Thread类的setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。
有了这个线程上下文加载器,JNDI 服务使用线程上下文加载器去加载所需要的 SPI 代码。也就是父类加载器请求子类加载器完成类加载的动作,这就打破了双亲委派模型。典型的例子有 JNDI 和 JDBC 等。
Tomcat 类加载机制
Tomcat 的类加载机制是违反了双亲委派原则的,对于一些为加载的非基础类(Object,String)等,各个 web 应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给 commonClassLoader 走双亲委派模型。整体结构如下:

这其中 JDK 提供的类加载器分别是:
- Bootstrap-启动类加载器,JVM 的一部分,加载 <JAVA_HOME>/lib/ 目录下特定的文件
- Extension-扩展类加载器,加载 <JAVA_HOME>/lib/ext/ 目录下的类库。
- Application-应用程序类加载器,也叫系统类加载器,加载 CLASSPATH 指定的类库。
Tomcat 自定义类加载器分别是:
- Common - 父加载器是 AppClassLoader,默认加载 ${catalina.home}/lib/ 目录下的类库
- Catalina - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 server.loader 配置的资源,一般是 Tomcat 内部使用的资源
- Shared - 父加载器是 Common 类加载器,加载 catalina.properties 配置文件中 shared.loader 配置的资源,一般是所有 Web 应用共享的资源
- WebappX - 父加载器是 Shared 加载器,加载 /WEB-INF/classes 的 class 和 /WEB-INF/lib/ 中的 jar 包
- JasperLoader - 父加载器是 Webapp 加载器,加载 work 目录应用编译 JSP 生成的 class 文件
JDBC 为什么要破坏双亲委派模型?
在 JDBC 4.0 之后我们需要使用 Class.forName 来加载驱动程序了,我们只需要把驱动的 jar 包放到工程的类加载路径里,那么驱动就会被自动加载。
这个自动加载采用的技术叫 SPI,可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名。我们只需要下面这一句话就可以创建数据库连接:
Connection con =
DriverManager.getConnection(url , username , password ) ;
因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到所需要的文件,这时候就需要委托子类加载器去加载 class 文件。
JDBC 的 Driver 接口定义在 JDK 中,其实现由各个数据库服务商来提供,比如 MySQL 的驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说 Bootstrap 类加载器还要去加载 jar 包中的 Driver 接口的实现类。Bootstrap 只负责 /lib/rt.jar 里面所有的 class,所以需要子类加载器去加载 Driver,这就破坏了双亲委派模型。
查看 DriverManager 类的源码,看到使用 DriverManager 的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用 ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的上下文加载器。
public Launcher() {
...
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);
...
}
可以看到,在 sun.misc.Launcher 初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是系统加载器。
Tomcat 为什么要破坏双亲委派模型
每个 Tomcat 的 webappClassLoader 加载自己目录的 class 文件,不会传递给父类加载器。
事实上,Tomcat 之所以造出了一堆自己的 classLoader,大致是出于三个目的:
- 对于各个 weapp 中的 class 和 lib,需要相互隔离,不能出现一个应用中加载的类影响另一个应用的情况。
- 与 JVM 一样出于安全考虑,使用单独的 classLoader 去装载 Tomcat 自身的类库,防止恶意或者无意的破坏。
- 热部署。
JVM:Java 类的加载机制的更多相关文章
- JVM:java类的加载机制
原文连接:https://www.cnblogs.com/ityouknow/p/5603287.html 类加载机制的奥妙. 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读 ...
- jvm系列(一):java类的加载机制
java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装 ...
- JVM(1):Java 类的加载机制
原文出处: 纯洁的微笑 java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang. ...
- jvm系列一、java类的加载机制
一.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...
- 02 Java类的加载机制
1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...
- Java 类的加载机制
1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...
- Java虚拟机(三):Java 类的加载机制
1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...
- java类的加载机制
什么是类装载器ClassLoader ClassLoader是一个抽象类 ClassLoader的实例将读入Java字节码将类装载到JVM中 ClassLoader可以定制,满足不同的字节码流获取方式 ...
- 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】
目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...
随机推荐
- 10 JavaScript对象&类&for循环
JavaScript对象 JavaScript中所有事物都是对象:字符串.数值.数组.函数.数学和正则表达式 JavaScript有些类型可以是字面量而非对象:如字符串.数值.布尔值 JavaScri ...
- TouchGFX
TouchGFX让用户界面代码只占用10KB的 SRAM空间和20KB的闪存空间的C ++软件框架.有无操作系统均可
- centos7中redis安装
官网地址:http://redis.io/ 官网下载地址:http://redis.io/download 1. 下载Redis源码(tar.gz),并上传到Linux:或 wget http://d ...
- 解题报告:luogu P5543 [USACO19FEB]The Great Revegetation S
题目链接:P5543 [USACO19FEB]The Great Revegetation S 好坑啊,都身败名裂了. 思路一: 考虑染色法,跑一遍搜所就好了,不给代码了. 思路二: 考虑并查集,我想 ...
- winform和wpf里必知的多线程知识
背景: 很多小伙伴经常在群里问线程的问题,平时我经常转一些视频教程这些人不看,我就自己写个总结吧 不过还是要注意的是,切换本来就不能太频繁,要一口气改. wpf的viewmodel就不需要UI线程,更 ...
- js中for循环(原生js)
1,普通for循环,经常用的数组遍历 var arr = [1,2,3,4,5]; for ( var i = 0; i <arr.length; i++){ console.log(arr[i ...
- Jmeter JDBC配置
前提条件,驱动包mysql-connector-java-5.1.38-bin.jar要放到本机Java路径:C:\Program Files\Java\jdk1.8.0_73\jre\lib\ext ...
- 为什么直接ping知乎的ip不能访问知乎的网站,而百度就可以?
结论: 简单的说,就是baidu有钱. 正文: 大型网站依靠自身稀稀落落的服务器很难满足网页"秒开"的用户需求,会加入CDN加速的队伍. 当用户访问 http://www.zhih ...
- 新闻网大数据实时分析可视化系统项目——14、Spark2.X环境准备、编译部署及运行
1.Spark概述 Spark 是一个用来实现快速而通用的集群计算的平台. 在速度方面, Spark 扩展了广泛使用的 MapReduce 计算模型,而且高效地支持更多计算模式,包括交互式查询和流处理 ...
- tensorflow之逻辑回归模型实现
前面一篇介绍了用tensorflow实现线性回归模型预测sklearn内置的波士顿房价,现在这一篇就记一下用逻辑回归分类sklearn提供的乳腺癌数据集,该数据集有569个样本,每个样本有30维,为二 ...