说一说JVM双亲委派机制与Tomcat
讲个故事:
以前,爱捣鼓的小明突然灵机一动,写出了下面的代码
package java.lang;
public class String {
//...复制真正String的其他方法
public boolean equals(Object anObject) {
sendEmail(xxx);
return equalsReal(anObject);
}
//...
}
这样,只要引用java.lang.String
的人,小明能随时收到他的系统的相关信息,这简直是个天才的注意。然而实施的时候却发现,JVM并没有加载这个类。
这是为什么呢?
小明能想到的事情,JVM设计者也肯定能想到。
双亲委派模型
上述故事纯属瞎编,不过,这确实是以前JVM存在的一个问题,这几天看Tomcat源代码的时候,发现频繁出现ClassLoader
为什么要用这个东西呢?
想要解答这个问题,得先了解一个定义:双亲委派模型。
这个词第一次看见是在《深入理解JVM》中,目的也是为了解决上面所提出来的问题。
在JVM中,存在三种类型的类加载器:
- 启动类(Bootstrap)加载器: 用于加载本地(
Navicat
)代码类的加载器,它负责装入%JAVA_HOME%/lib
下面的类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 - 标准扩展(Extension)类加载器: 由
ExtClassLoader
实现,负责加载%JAVA_HOME/lib/ext%
或者系统变量java.ext.dir
(可使用System.out.println("java.ext.dir")查看
)指定的类加载到内存中 - 系统(System)类加载器: 由
AppClassLoader
实现,负责加载系统类(环境变量%CLASSPATH%
)指定,默认为当前路径的类加载到内存中。
除去以上三种外,还有一种比较特殊的线程上下文类加载器。存在于Thread
类中,一般使用方式为new Thread().getContextClassLoader()
可以看出来,三种类型的加载器负责不同的模块的加载。那怎么才能保证我所使用的String
就是JDK里面的String
呢?这就是双亲委派模型的功能了:
上面三种类加载器中,他们之间的关系为:
也就是Bootstrap ClassLoader
作为Extension ClassLoader
的父类,而Extension ClassLoader
作为Application ClassLoader
的父类,Application ClassLoader
是作为User ClassLoader
的父类的。
而双亲委派机制规定:当某个特定的类加载在接收到类加载的请求的时候,首先需要将加载任务委托给父类加载器,依次递归到顶层后,如果最高层父类能够找到需要加载的类,则成功返回,若父类无法找到相关的类,则依次传递给子类。
补充:
- 如果A类引用了B,则JVM将使用加载类A的加载器加载类B
- 类加载器存在缓存,如果某个加载器以前成功加载过某个类后,再次接受到此类加载请求则直接返回,不再向上传递加载请求
- 可以通过
ClassLoader.loadClass()
或Class.ForName(xxx,true,classLoader)
指定某个加载器加载类 - 类类型由加载它的加载器和这个类本身共同决定,如果类加载器不同,类名相同,
instanceof
依然会返回false
- 父加载器无法加载子加载器能够加载的类
可以看到,通过双亲委派机制,能够保证使用的类的安全性,并且可以避免类重名的情况下JVM存在多个相同的类名相同,字节码不同的类。
回到刚开始讲的故事,虽然小明自定义了
String
,包名也叫java.lang
,但是当用户使用String
的时候,会由普通的Application ClassLoader
加载java.lang.String
,此时通过双亲委派,类加载请求会上传给Application ClassLoader
的父类,直到传递给Bootstrap ClassLoader
,而此时,Bootstrap ClassLoader
将在%JAVA_HOME%/lib中寻找java.lang.String
而此时正好能够找到java.lang.String
,加载成功,返回。因此小明自己写的java.lang.String
并没有被加载。
可以看见,如果真的想要实现小明的计划,只能将小明自己编写的
java.lang.String
这个class
文件替换到%JAVA_HOME%/lib/rt.jar 中的String.class
自定义ClassLoader
到这里,估计能明白为什么需要双亲委派模型了,而某些时候,我们可以看见许多框架都自定义了ClassLoader
,通过自定义ClassLoader
,我们可以做很多好玩的事情,比如:设计一个从指定路径动态加载类的类加载器:
public class DiskClassLoader extends ClassLoader {
private String libPath;
public DiskClassLoader(String path){
libPath=path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try(FileInputStream fileInputStream=new FileInputStream(new File(libPath,getFileName(name)));
BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream);
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream()){
for (int len=0;(len=bufferedInputStream.read())!=-1;){
byteArrayOutputStream.write(len);
}
byte[] data=byteArrayOutputStream.toByteArray();
return defineClass(name,data,0,data.length);
}catch (IOException e){
e.printStackTrace();
}
return super.findClass(name);
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
上面是一个简单的例子,可以看见想要自定义ClassLoader
,只需要继承ClassLoader
,然后覆盖findClass()
方法即可,其中findClass()
是负责获取指定类的字节码的,在获取到字节码后,需要手动调用defineClass()
加载类。
在ClassLoader
类中,我们能找到loadClass
的源代码:
protected Class<?> loadClass(String name, boolean resolve) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
在删减掉一些模板代码后,我们可以看到loadClass()
方法就是实现双亲委派的主要代码:首先查看类是否有缓存,如果没有,就调用父类的loadClass
方法,让父类去加载,如果父类加载失败,则自己加载,如果自己加载失败,那就返回null
,注意:并没有再找自己的子类去寻找类,也就是在哪里发起的加载,就在哪里结束。
这里可以看到,
loadClass()
方法并没有被标记为final
的,也就是我们依然可以重载它的loadClass()
方法,破坏原本的委派双亲模型。
破坏双亲委派机制
有些时候,双亲委派机制也会遇到一些问题,在介绍双亲委派机制的时候,我列举了一些补充。而在一些JDK中,存在一些基础API
他们的加载由比较上层的加载器负责,这些API
只是一些简单的接口,而具体的实现可能会由其他用户自己实现,这个时候就存在一个问题,如果这些基础的API
需要调用/加载用户的代码的时候,会发现由于父类无法找到子类所能加载的类的原因,调用失败。
最典型的例子便是JNDI
服务,JNDI
服务是在JDK1.3
的时候放入rt.jar
中,而rt.jar
有Bootstrap ClassLoader
加载,JNDI
的功能是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath
下的JNDI
接口提供者(SPI, Service Provider Interface)
的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
这就需要用到最开始讲的特殊的加载器:上下文类加载器
上下文类加载器的使用方式为:Thread.currentThread().getContextClassLoader()
上下文类加载器是什么意思呢?可以看源码,Thread
初始化是通过本地方法currentThread();
初始化的,而classLoader
也正是通过currentThread
初始化,currentThread
指的是当前正在运行的线程。
而默认情况下,启动Launcher
后,Launcher
会将当前线程的上下文加载器设置为Application ClassLoader
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);
//...
}
因此,上下文类加载器默认就是系统加载器,通过上下文加载器,更高级别的加载器便可以调用系统加载器加载一个类。
Tomcat 与类加载器
Tomcat
作为一个Web容器,会包含各种Web应用程序,而为了使各个应用程序不互相干扰,至少需要达到以下要求:
- 部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离
- 部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以相互共享
- Web容器需要保证自身的安全不受Web应用程序所影响
- 只是JSP的容器,需要支持热部署功能
因为这些需求,所以在Tomcat中,类的加载不能使用简单的ClassLoader
来加载,而是需要自定义分级的ClassLoader
。
在Tomcat中,定义了3组目录结构/common/*
,/server/*
和/shared/*
可以存放Java类库,另外还有Web应用程序自身的结构:/WEB-INF/*
,而这几级目录结构分别对应了不同的加载器
- common: 类库可以被Tomcat和所有Web应用程序共同使用
- server: 类库可以被Tomcat使用,对其他Web程序不可见
- shared: 类库可以被所有的Web应用程序共同使用,但对Tomcat不可见
- **WEB-INF: ** 类库仅仅能被自身Web应用程序使用
因此,需要支持以上结构,可以通过自定义遵循双亲委派模型的ClassLoader
来完成。
参考链接:
如果觉得写得不错,欢迎关注微信公众号:逸游Java ,每天不定时发布一些有关Java干货的文章,感谢关注
说一说JVM双亲委派机制与Tomcat的更多相关文章
- [转帖]说一说JVM双亲委派机制与Tomcat
说一说JVM双亲委派机制与Tomcat https://www.cnblogs.com/dengchengchao/p/11844022.html 讲个故事: 以前,爱捣鼓的小明突然灵机一动,写出了下 ...
- jvm双亲委派机制详解
双亲委派机制 记录一下JVM的双亲委派机制学习记录. 类加载器种类 当我们运行某一个java类的main方法时,首先需要由java虚拟机的类加载器将我们要执行的main方法所在的class文件 ...
- JVM加载类的过程,双亲委派机制中的方法
JVM加载类的过程: 1)JVM中类的整个生命周期: 加载=>验证=>准备=>解析=>初始化=>使用=>卸载 1.1.加载 类的加载阶段,主要是获取定义此类的二进 ...
- 深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题
一.概述 定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的 ...
- JVM学习六:JVM之类加载器之双亲委派机制
前面我们知道类加载有系统自带的3种加载器,也有自定义的加载器,那么这些加载器之间的关系是什么,已经在加载类的时候,谁去加载呢?这节,我们将进行讲解. 一.双亲委派机制 JVM的ClassLoader采 ...
- JVM之类加载器、加载过程及双亲委派机制
JVM 的生命周期 虚拟机的启动 Java 虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实 ...
- JVM类加载与双亲委派机制被打破
前言 前文已经讲了虚拟机将java文件编译成class文件后的格式:JVM虚拟机Class类文件研究分析 java文件经过编译,形成class文件,那么虚拟机如何将这些Class文件读取到内存中呢? ...
- JVM探究(一)谈谈双亲委派机制和沙箱安全机制
JVM探究 请你谈谈你对JVM的理解?java8虚拟机和之前的变化gengxin? 什么是OOM,什么是栈溢出StackOverFlowError JVM的常用调优参数有哪些? 内存快转如何抓取,怎么 ...
- 面试~jvm(JVM内存结构、类加载、双亲委派机制、对象分配,了解垃圾回收)
一.JVM内存结构 ▷ 谈及内存结构各个部分的数据交互过程:还可以再谈及生命周期.数据共享:是否GC.是否OOM 答:jvm 内存结构包括程序计数器.虚拟机栈.本地方法栈.堆.方法区:它是字节码运行时 ...
随机推荐
- MySQL日期和时间类型笔记
最近在看<MySQL技术内幕:SQL编程>并做了笔记,这是一篇笔记类型博客,分享出来方便自己复习,也可以帮助其他人 一.日期时间类型所占空间对比 各种日期时间数据类型所占的空间: 类型 所 ...
- 控制器向视图传参ModelAndView、Model和Map
ModelAndView类 ModelAndView在spring-webmvc-4.3.18.RELEASE.jar包下,当然其他版本也有,所在包如下 对于那些返回String等类型的处理方法,sp ...
- oracle表空间不足:ORA-01653: unable to extend table
问题背景: oracle表空间不足报错是比较常见的故障,尤其是没有对剩余表空间做定期巡检的系统: 报错代码如下: oracle表空间不足错误代码:ORA-01653: unable to extend ...
- JAVA之类的动手动脑
1.默认构造方法与自定义的构造方法的冲突 package com.xu; class fool { int value; fool(int nowvalue) { value=nowvalue; } ...
- C++学习笔记-预备知识
1.1 C++简介 C++融合3种不同的编程方式:C语言代表的过程性语言.C++在C语言基础上添加的类代表的面向对象语言.C++模板支持的广泛编程. 1.2 C++简史 1.2.1 C语言 Ritch ...
- 考试题string——线段树。
string[题目描述]给定一个由小写字母组成的字符串 s.有 m 次操作,每次操作给定 3 个参数 l,r,x.如果 x=1,将 s[l]~s[r]升序排序;如果 x=0,将 s[l]~s[r]降序 ...
- python3爬虫环境搭建
安装python3 sudo apt-get install python3-dev build-essential libssl-dev libffi-dev libxml2 libxml2-dev ...
- Integer类型与int的==比较
前言 Java中有两种类型 基本类型 基本数据类类型存的是数值本身 引用类型 引用类型变量在内存放的是数据的引用 基本类型通过==比较的是他们的值大小,而引用类型比较的是他们的引用地址 正文 在一些特 ...
- 解决vue组件内前置路由守卫beforeRouteEnter无法获取上下文this
问题描述 vue框架,只有在报名页面报名成功,然后自动跳转到订单详情,才弹出一个引流弹窗,其他情况均不弹出,我就想到使用vue 的组件内前置守卫beforeRouteEnter来实现.beforeRo ...
- 聊聊db和缓存一致性的5种实现方式
数据存储在数据库中,为了加快业务访问的速度,我们将数据库中的一些数据放在缓存中,那么问题来了,如何确保db和缓存中数据的一致性呢?我们列出了5种方法,大家都了解一下,然后根据业务自己选择. 方案1 获 ...