发布时间:2024-07-26 19:01
Java-底层原理-编译原理
Java-底层原理-javac源码笔记
Java-底层原理-类加载机制
Java-底层原理-clinit和init
前面写了类编译相关文章Java-底层原理-编译原理和Java-底层原理-编译原理,知道了怎么从java文件到class文件的过程。
本文简要介绍从class文件到JVM内存过程即Java加载机制,还会介绍双亲委派机制的破坏,线程上下文加载器,以及JDBC Driver是如何自动加载的。
未完成
广义上的加载包括加载、连接、初始化三个阶段:
以上几个阶段开始(是开始,而并不一定是一个完成下一个才开始)顺序固定,但需要注意的是解析阶段可能在初始化之后再进行,以支持运行期动态绑定。
下图是验证准备解析三个阶段的详图,展示了从.class
文件到Class
对象的过程:
.class->内存
)加载工作由ClassLoader负责,读取class字节码文件,创建Class对象,放入内存:
Class.forName("类全限定名")
xxxClass.Class
classLoader.loadClass("类全限定名");
验证->准备->解析
)验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用(符号引用->直接引用)。
连接又可分为验证、准备、解析三个阶段。
验证.class
字节码文件中的信息符合当前虚拟机规范,且不含危害虚拟机行为:
.class
文件能正确解析,验证通过就存入方法区.class
文件中的描述信息进行语义分析,检查是否符合规范,比如继承了final类或方法解析
阶段后。**检查符号引用中的访问性等为类的静态字段分配方法区内存,并设初值。但要注意final和非final有所不同:
static int i = 123
,在准备阶段只会设 i 初值为0。而在初始化阶段才会将 i 设值为123。putstatic
指令,在程序编译阶段存放于
,等到类初始化阶段才会执行。主要是符号引用转为直接引用,主要针对类或接口、字段、类方法(static method)、接口方法、方法类型、方法句柄和调用点限定符。
在编译阶段,无法确定具体的内存地址,所以会使用符号来代替,即对抽象的类或接口进行符号填充为符号表。在类加载的解析阶段,JVM 将符号替换成具体的内存地址,也就是符号引用转直接引用。
.class
文件的常量池。clinit->Class对象
)初始化阶段是类加载的最后一步,此时才会真正开始执行java应用程序代码(字节码)。此阶段中,会真正为类变量(static 非final)赋初值,以及做其他资源的初始化工作。
总的来说,初始化阶段会执行类构造器即
方法。
虚拟机规范规定以下情况需要执行类初始化,这些行为称为主动引用
new
(创建类,但不是创建数组)、getstatic
(读类变量)、putstatic
(写类变量)、invokestatic
(调用类方法)4条字节码指令,如果目标类未初始化,就需要执行初始化:
jdk1.7
动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic
的方法句柄,并且这个句柄所在类还未初始化,则会先触发其初始化。除主动引用情况以外的称为被动引用,不会触发类初始化,比如
.class
文件后,调用类对该常量的引用就变为对自己类的常量池的引用了,和该常量的出处类无关了。接口内也有
用来初始化接口中的类变量,但接口与类不同的是,接口初始化时无需先初始化父接口。而是要用到其类变量时时才会初始化。
当前版本jdk是采用双亲委派机制:
BootstrapClassLoader
。BootstrapClassLoader
ExtClassLoader
AppClassLoader
ClassLoader
ClassLoader加载Class大概思路如下:
ClassNotFoundException
。ClassLoader源码解析在第3章
BootstrapClassLoader
是启动类加载器,由C++实现。
当使用System.out.println(obj.getClass().getClassLoader());
时结果为null
。
通过以下代码打出BootstrapClassLoader
加载的文件:
public class BootStrapTest
{
public static void main(String[] args)
{
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
System.out.println("--------------------------");
System.out.println("sun.boot.class.path=\n" + System.getProperty("sun.boot.class.path"));
}
}
结果如下:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes
--------------------------
sun.boot.class.path=
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes
sun.boot.class.path
也可以看到BootstrapClassLoader
加载的文件。除了rt.jar
以外还加载了一些其他文件。
扩展类加载器,加载jre/lib/ext
下的文件。
该类属于sun.misc.Launcher
的子类,继承自java.net.URLClassLoader
:
static class ExtClassLoader extends URLClassLoader
当使用getParent()
的时候是null
,表示是被BootstrapClassLoader
加载,即BootstrapClassLoader
是ExtClassLoader
的父加载器。
加载classpath
下的jar
和class
文件,由由-classpath
或-cp
或JAR中的Manifest
的classpath
属性定义,一般是用户代码及其依赖。
该类也属于sun.misc.Launcher
的子类,同样继承自java.net.URLClassLoader
:
static class AppClassLoader extends URLClassLoader
当使用getParent()
的时候是ExtClassLoader
,表示即ExtClassLoader
是AppClassLoader
的父加载器。
但需要注意的是,AppClassLoader
位于rt.jar
,也是被BootstrapClassLoader
加载。
推荐继承java.lang.ClassLoader
重写findClass
方法,而不是直接重写loadClass
,否则可能破坏原有的双亲委派加载机制。
在某些时候,需要定制加载逻辑,就需要自己开发ClassLoader。比如位于网络的字节码文件。或是加密了的字节码文件。
下面是一个简单示例:
public class MyClassLoader extends ClassLoader {
private String path = null;
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadByte(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("类找不到" + name);
}
}
private byte[] loadByte(String name) throws Exception {
String fileName = path + File.separator + replaceSeparator(name) + ".class";
try (FileInputStream fis = new FileInputStream(fileName)) {
byte[] data = new byte[fis.available()];
fis.read(data);
return data;
} catch (Exception e) {
throw e;
}
}
private String replaceSeparator(String name) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
if (name.charAt(i) != '.') {
sb.append(name.charAt(i));
} else {
sb.append(File.separator);
}
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("/home/wyj");
Runtime.getRuntime().exec("javac /home/wyj/Test.java");
Class<?> test = classLoader.loadClass("Test");
System.out.println(test.getClassLoader());
}
}
AppClassLoader
和ExtClassLoader
两个类本身由谁加载。public class Test2 {
public static void main(String[] args) {
ClassLoader appClassLoader = Test2.class.getClassLoader();
System.out.println("appClassLoader=" + appClassLoader);
System.out.println("appClassLoader.getClass().getClassLoader()=" + appClassLoader.getClass().getClassLoader());
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("appClassLoader.getParent()=" + extClassLoader);
System.out.println("extClassLoader.getClass().getClassLoader()=" + extClassLoader.getClass().getClassLoader());
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("extClassLoader.getParent()=" + bootstrapClassLoader);
}
}
appClassLoader=sun.misc.Launcher$AppClassLoader@18b4aac2
appClassLoader.getClass().getClassLoader()=null
appClassLoader.getParent()=sun.misc.Launcher$ExtClassLoader@2781e022
extClassLoader.getClass().getClassLoader()=null
extClassLoader.getParent()=null
jre/lib/etx
目录下的jar文件。public class ClassLoaderTest2 {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest2.class.getClassLoader();
while(loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
System.out.println(loader);
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@75412c2f
null
将该java文件打包成jar,移动到jre/lib/ext
后再次运行结果如下:
$ java -jar jartest.jar
sun.misc.Launcher$ExtClassLoader@7852e922
null
jre/lib/etx
目录下的jar文件,而且可把自定义的代码放入该文件,一样会被ExtClassLoader加载,而不再被AppClassLoader加载。比如java.lang.Object
,用户也自定义一个同样权限定名的类。但在加载时,会首先用BootstrapClassLoader
加载rt.jar
中的该类。
而用户自定义的同包名Object类,也会因为AppClassLoader
往上寻找到祖先BootstrapClassLoader
类来加载该类,但会发现该类已经被加载过导致报错。
但是你可以用重写loadClass
等方法来打破这种双亲委派机制。
Java中判断两个类是否相同条件有2:
JDK1.2之前没有双亲委派模型,所以之前的开发者继承java.lang.ClassLoader
后要做的就是重写loadClass()
方法,编写自定义的类加载逻辑。
ThreadContextClassLoader即线程上下文加载器
Java中的委派链是:
AppClassLoader -> ExtensionClassLoader -> BootstrapClassLoader
也就是说,委派链左边的ClassLoader加载出来的类可以很自然的使用右侧的ClassLoader加载出来的类对象。
但反过来就不行,比如JNDI
服务(用于对资源进行集中管理和查找)位于rt.jar
(由BootstrapClassLoader
加载),它需要调用独立厂商实现部署在应用程序的classpath
下的JNDI接口提供者(SPI, Service Provider Interface)的代码(应该由AppClassLoader
加载),但BootstrapClassLoader
无法加载这些用户代码,所以引入了线程上下文件类加载器(ThreadContextClassLoader
)。有了他,JNDI服务就可以去加载所需要的SPI
代码了,即父加加载器请求子加载器去完成类加载。实际上这就破坏了双亲委派机制。
如果没有通过setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的ThreadContextClassLoader
。这个是在线程对象init的时候做的。
Java应用运行时的初始ThreadContextClassLoader
就是AppClassLoader
。
除了JNDI,JDBC, JCE, JAXB, JBI
等设计SPI的加载动作基本都采用此方式。
我们通常使用的jdbc连接代码如下:
Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "pwd");
执行Class.forName
会使用当前调用类的ClassLoader(AppClassLoader
)来加载com.mysql.jdbc.Driver
,该Driver
的
如下:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
因为此时DriverManager
还未加载过,且这里调用了其静态方法,所以又会去加载、初始化DriverManager
。注意该DriverManager
位于java.sql
包中,打在rt.jar
内,所以会因为双亲委派机制从而被BootstrapClassLoader
加载。DriverManager
初始化时的
方法代码如下:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在loadInitialDrivers
方法中有如下代码:
// 这里的Driver是java.sql.Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
该load方法如下:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 这里就设置用了线程上下文加载器,即当前用户主线程的AppClassLoader来加载类
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
后续加载用户用SPI注册的各Driver类时,就是用当前设置的线程上下文加载器AppClassLoader
来加载。
具体可以参考Java-SPI
第三次破坏源于用户对程序动态性的追求而导致的。这里的动态性指代码热替换HotSwap
, 模块热部署
等。
OSGI是业界中的Java模块化标准,实现模块化的热部署的关键则是它自定义的类加载机制的实现,即每个程序模块都要一个自己的类加载器。当需热替换时,就把程序模块连同加载器一起换掉。
在OSGi环境下,类加载器不再是双亲委派中的树状结构,而是进一步发展为网状结构。
ClassLoader的可见性是指:
小例子如下:
public class ClassLoaderTest {
public static void main(String args[]) {
try {
// print ClassLoader of this class
System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
+ ClassLoaderTest.class.getClassLoader());
// trying to explicitly load this class again using Extension class loader
Class.forName("test.ClassLoaderTest", true
, ClassLoaderTest.class.getClassLoader().getParent());
} catch (ClassNotFoundException ex) {
Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
该例子会抛出ClassNotFoundException
,也就是说ExtClassLoader
不能看见其子ClassLoader AppClassLoader
所加载的类。
ClassLoader的单例性是指:
loadClass
方法破坏双亲委派机制,从而使得一个JVM内,一个类有多个不同ClassLoader加载生成的Class对象放在内存。但需要注意的是,他们之间不能互相转换!两个对象的Class类型是否相同,判断条件有两个:
以下代码可以简单验证:
自定义类加载器如下,注意重写了loadClass
方法,破坏了双亲委派机制:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Objects;
public class CustomClassLoader extends ClassLoader
{
private static String BASE_PATH = CustomClassLoader.class.getResource("/").getPath();
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException
{
try {
Class findClass = findLoadedClass(name);
if (!Objects.isNull(findClass)) {
return findClass;
}
//包+类完整磁盘路径
String filePath = BASE_PATH + name.replace(".", File.separator) + ".class";
File file = new File(filePath);
if(!file.exists()){
return super.loadClass(name);
}
try(FileInputStream fis = new FileInputStream(file)){
if(fis == null){
return super.loadClass(name);
}
byte[] b = new byte[fis.available()];
fis.read(b);
return defineClass(name, b, 0, b.length);
}
}
catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
}
测试代码如下:
public class CusClassLoaderClassConvertTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myLoader = new CustomClassLoader();
String subCompanyFullName = "demos.classInitialization.classloader.custom.SubCompany";
SubCompany subCompany = (SubCompany)(myLoader.loadClass(subCompanyFullName).newInstance());
}
}
最终输出如下:
Exception in thread "main" java.lang.ClassCastException:
demos.classInitialization.classloader.custom.SubCompany cannot be cast
to demos.classInitialization.classloader.custom.SubCompany
at demos.classInitialization.classloader.custom.CusClassLoaderClassConvertTest.main(CusClassLoaderClassConvertTest.java:7)
可以看到,全限定相同的类因为ClassLoader不同,导致转换失败,抛出异常ClassCastException
父加载器加载的类不能直接使用未加载的子加载器加载的类。
比如有一个由AppClassLoader加载的A类,使用一个由来自于网络上的类B,由于此时只能用当前A类进行父类委托加载,所以无法加载来自于网络的类B,而必须提前用专用的网络类ClassLoader加载B后才能使用!这又就是所谓隔离性。
位于不同分支的不同ClassLoader加载的类不能相互引用。要了解这个原因,先认识几个概念:
指真正加载类的ClassLoader。如java.lang.Object
的定义类加载器就是BootstrapClassLoader
。
指参与加载类的ClassLoader。如java.lang.Object
的初始类加载器就是BootstrapClassLoader
, ExtClassLoader
和 AppClassLoader
(父类委托)。
JVM为每个ClassLoader实例维护了一个类似表的结构,记录该ClassLoader实例作为初始类加载器
参与加载的所有类,这称为命名空间,可隔离不同类加载器实例加载的Class文件。
注意是ClassLoader实例,比如AppClassLoader加载器有两个实例对象loader1和loader2,那么这两个实例对象加载的Class文件也是互相隔离。
所以用户代码可以使用java.lang.Object
,是因为他们都在AppClassLoader
的命名空间中。
而反过来,java.lang.Object
还在BootstrapClassLoader
和ExtClassLoader
两个命名空间,所以不能直接访问用户代码。
加载类的主要的方法是loadClass
,代码如下:
// 父加载器
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 先在ClassLoader获取该ClassName对应的同步锁对象
// 注意,这里的并行度是按每个类锁定
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 先检查该类全限定名是否已经被加载到JVM
Class<?> c = findLoadedClass(name);
if (c == null) {
// 此时没有加载该类
long t0 = System.nanoTime();
try {
if (parent != null) {
// 存在父加载器(不是BootStrapClassLoader)
// 就尝试让父加载器加载类,但不连接
c = parent.loadClass(name, false);
} else {
// 否则尝试用BootStrapClassLoader加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 此时还没有加载该类
long t1 = System.nanoTime();
// 就调用findClass方法来查找该类
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;
}
}
但在JDK1.2之后,不再提倡重写loadClass()
,而是应该重写新加入的findClass
方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 该方法默认没有实现,只是抛出一个ClassNotFoundException
throw new ClassNotFoundException(name);
}
启动类加载器BootstrapClassLoader
是用C++实现,而扩展类加载器ExtClassLoader
和应用程序类加载器AppClassLoader
都继承自URLClassLoader
。findClass
方法直接用的URLClassLoader
的findClass
方法:
// 要加载的类的全限定名
// 比如是demos.classInitialization.classloader.order.EntityC
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 那么这里path为demos/classInitialization/classloader/order/EntityC.class
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 使用从指定Resource获取的类字节来转为Class对象
// 必须先连接,然后生成的类才能使用它。
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
// 如果结果为空,直接抛ClassNotFoundException
throw new ClassNotFoundException(name);
}
// 返回得到的Class对象
return result;
}
接着看看URLClassLoader
的defineClass
方法:
/*
* 使用从指定的资源中获取的class bytes 来定义一个Class对象
* 最终得到的Class必须在使用前被解析
*/
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
// file:/xxx/javaDemos/target/classes/
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
// 检查包是否已经加载过
Manifest man = res.getManifest();
//
definePackageInternal(pkgname, man, url);
}
// 从class字节码中读取数据并转为Class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// 直接使用ByteBuffer读取(字节缓冲)
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}
所有继承自ClassLoader且没有重写getSystemClassLoader
方法的ClassLoader,通过getSystemClassLoader
方法得到的AppClassLoader
都是同一个实例,类似单例模式。
// system ClassLoader
private static ClassLoader scl;
// 一旦system ClassLoader被设置后,就把sclSet设为true
private static boolean sclSet;
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
// 初始化SystemClassLoader(AppClassLoader)
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
// 返回的就是AppClassLoader
return scl;
}
其实ClassLoader加载类的流程就是以下几步:
ClassLoader.loadClass
方法ClassNotFoundException
resolveClass
方法连接该Class对象主流的Java Web服务器如Tomcat, Jetty等都实现了自定义的类加载器,原因如下:
HotSwap
功能。Tomcat 7.x,目录结构如下:
与许多服务器应用相同,Tomcat有各种类加载器(实现自java.lang.ClassLoader
),以允许容器的不同部分和容器上运行的Web应用程序能访问不同的用于存放class和资源文件的库。此机制用于提供Servlet规范2.4版中定义的功能。
在Web应用中的ClassLoader机制和Java中的双亲委派机制有少许不同,但总体原则相同。Tomcat7.x在启动时会创建一组ClassLoader,也有父子关系,模型如下:
$JAVA_HOME/jre/lib/ext
下的扩展jar文件。SystemClassLoader
从CLASSPATH
的环境变量初始化。**所有通过SystemClassLoader加载的类都对Tomcat内部类和Web应用程序可见。**但使用bin/catalina.sh
启动时会忽略CLASSPATH
环境变量,而是从以下位置构建SystemClassLoader:
main()
方法,以及它依赖的ClassLoader实现类。Apache Commons Daemon
项目的类。 这个JAR文件不存在于catalina.bat | .sh
脚本构建的CLASSPATH
中,而是从bootstrap.jar
的manifest
文件中引用。CommonClassLoader
包含对Tomcat内部类和所有Web应用程序都可见的其他类。conf/catalina.properties
中的common.loader
属性定义。 默认设置将按列出的顺序搜索:${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
WEB-INF/classes
里的class和资源文件以及WEB-INF/lib
里的jar文件仅对当前单个Web应用程序可见,而对其他Web应用不可见,保证隔离性。如上所述,Tomcat的Web ClassLoader模型与Java的不同(根据Servlet规范2.4版,第9.7.2节Web应用程序类加载器中的建议)。 当处理Web应用程序的WebappX ClassLoader发起的加载类的请求时,此ClassLoader将首先在本地库查找,而不是像Java委派模型那样委派给父加载器。
有一种例外情况,即作为JRE基类的一部分的那些类不能被覆盖。 对于某些类(例如J2SE 1.4+中的XML解析器组件),Java认可的功能可以用于Java 8.
最后,类加载器将显式忽略包含Servlet API
类的任何JAR文件 - 也就是说,不要在Web应用程序中包含此类JAR。
Tomcat中除了上面提到的WebappX ClassLoader(处理Web应用程序的WebappX ClassLoader
发起的加载类的请求时,此ClassLoader将首先在本地库查找,而不是像Java委派模型那样委派给父加载器。)以外,所有其他的几种ClassLoader都遵循双亲委派模式。
因此,默认情况下,从Web应用程序的角度来看,类或资源文件的加载将按以下顺序查找以下存储库:
/WEB-INF/classes
/WEB-INF/lib/*.jar
而如果配置了
表示采用双亲委派机制,那就和传统的Java加载顺序相同,具体顺序如下:
/WEB-INF/classes
/WEB-INF/lib/*.jar
此外,还可以配置更复杂的共享模式架构:
默认情况下,未定义Server
和Shared
ClassLoader。 通过在conf/catalina.properties
中定义server.loader
和或shared.loader
属性的值,可以使用这种更复杂的层次结构:
ServerClassLoader
加载的类值对Tomcat内部类可见,对用户Web应用程序不可见。相当于Tomcat专属ClassLoader。SharedClassLoader
对所有用户Web应用程序都可见,可用在他们之间共享代码。然而,任何对shared代码的修改都需要Tomcat服务重启。还可以参考这篇文章Tomcat7源码解读(四) —— 类加载器1
来自Apache Zeppelin 的org.apache.zeppelin.plugin.PluginManager
的一段代码,用来加载不在classPath的plugin jar的代码:
// launcherPlugin为目标类全限定名
public synchronized InterpreterLauncher loadInterpreterLauncher(String launcherPlugin,
RecoveryStorage recoveryStorage)
throws IOException {
// 存储实例对象缓存
if (cachedLaunchers.containsKey(launcherPlugin)) {
return cachedLaunchers.get(launcherPlugin);
}
String launcherClassName = "org.apache.zeppelin.interpreter.launcher." + launcherPlugin;
LOGGER.info("Loading Interpreter Launcher Plugin: " + launcherClassName);
// 如果目标类属于系统自带的,则已加入classpath,可直接用AppClassLoader加载
if (builtinLauncherClassNames.contains(launcherClassName) ||
Boolean.parseBoolean(System.getProperty("zeppelin.isTest", "false"))) {
try {
InterpreterLauncher launcher = (InterpreterLauncher)
(Class.forName(launcherClassName))
.getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
.newInstance(zConf, recoveryStorage);
return launcher;
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException
| NoSuchMethodException | InvocationTargetException e) {
throw new IOException("Fail to instantiate InterpreterLauncher from classpath directly:"
+ launcherClassName, e);
}
}
// 获取填充了要加载的目标地址的URLClassLoader实例
URLClassLoader pluginClassLoader = getPluginClassLoader(pluginsDir, "Launcher", launcherPlugin);
InterpreterLauncher launcher = null;
try {
// 使用URLClassLoader加载目标类、初始化,此后会将所有url list的jar加入classpath
launcher = (InterpreterLauncher) (Class.forName(launcherClassName, true, pluginClassLoader))
.getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
.newInstance(zConf, recoveryStorage);
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException
| NoSuchMethodException | InvocationTargetException e) {
throw new IOException("Fail to instantiate Launcher " + launcherPlugin +
" from plugin pluginDir: " + pluginsDir, e);
}
// 将加载好的类实例放入缓存
cachedLaunchers.put(launcherPlugin, launcher);
return launcher;
}
// 获取填充了要加载的目标地址的URLClassLoader实例
private URLClassLoader getPluginClassLoader(String pluginsDir,
String pluginType,
String pluginName) throws IOException {
// 拼接目录路径,如/zeppelin/plugins/Launcher/YarnInterpreterLauncher
File pluginFolder = new File(pluginsDir + "/" + pluginType + "/" + pluginName);
if (!pluginFolder.exists() || pluginFolder.isFile()) {
LOGGER.warn("PluginFolder " + pluginFolder.getAbsolutePath() +
" doesn't exist or is not a directory");
return null;
}
List<URL> urls = new ArrayList<>();
// 遍历目标目录,将下面的所有jar全部添加到url list
for (File file : pluginFolder.listFiles()) {
LOGGER.debug("Add file " + file.getAbsolutePath() + " to classpath of plugin: "
+ pluginName);
urls.add(file.toURI().toURL());
}
if (urls.isEmpty()) {
LOGGER.warn("Can not load plugin " + pluginName +
", because the plugin folder " + pluginFolder + " is empty.");
return null;
}
// 返回填充了要加载的目标地址的URLClassLoader实例
return new URLClassLoader(urls.toArray(new URL[0]));
}
-《深入理解JVM虚拟机》
详细深入分析 Java ClassLoader 工作机制
深入分析Java ClassLoader原理
Java 自定义ClassLoader
Java双亲委派模型及破坏
Apache Tomcat 7
类加载器的工作原理
Java类加载器机制-双亲委派模型详细的答疑
类加载器的父亲委托机制深度详解