概念
类加载器是java虚拟机提供给应用程序去实现获取类和接口自己字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存的这一部分。
本地接口:允许JAVA调用其他语言编写的方法。在hotspot类加载器中,主要用于调用Java虚拟机中的方法,这些方法使用C++编写。
应用场景
- 企业级应用:SPI机制,类的热部署,Tomcat类的隔离
- 解决线上问题:使用Arthas不停机解决线上故障
启动类加载器
- 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。
- 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar等
通过启动类加载器去加载用户jar包
- 加入jre/lib下进行扩展(不推荐)
不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地加载。
- 使用参数进行扩展(推荐)
启动类加载器使用 -Xbootclasspath /a:jar包目录/jar包名进行扩展
扩展类加载器和应用程序类加载器
- 扩展类加载器和应用程序类加载器都是JDK中提供的、使用Java编写的类加载器。
- 它们的源码都位于sun.mics.Launcher中,是一个静态内部的类。继承自URLCLassLoader。具备通过目录或者指定Jar包将字节码文件加载到内存中。
扩展类加载器
- 扩展类加载器是JDK中提供的、使用JAVA编写的类加载器。
- 默认加载Java安装目录/jre/lib/ext下的类文件。
通过扩展类加载器去加载用户jar包
- 放入/jre/lib/ext下进行扩展
不推荐,尽可能不要去更改JDK安装目录中的内容
- 使用参数进行扩展
推荐,使用-Djava.ext.dirs = jar包目录进行扩展,这种方式会覆盖掉原目录。可以用;(原始目录)方式追加上原始目录。
应用程序类加载器
应用程序类加载器(Application Class Loader)是Java虚拟机(JVM)的一种类加载器,也是ClassLoader的子类,它负责从CLASSPATH环境变量中指定的路径或JAR文件加载类,通常也称为系统类加载器。
应用程序类加载器加载类的方式
- 从classpath中加载类文件
运行时自动将CLASSPATH中指定的路径下的类文件加载到JVM中。可以执行以下代码查看 CLASSPATH路径:
String classpath = System.getProperty("java.class.path");
System.out.println(classpath);
- 使用addURL()方法加载Jar包
可以使用以下代码加载JAR包:addURL()方法会将该JAR文件添加到应用程序类路径中,这样就可以使用类加载器加载该JAR文件中的类了。
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
URL jarUrl = new URL("file://test.jar");
appClassLoader.addURL(jarUrl);
类的双亲委派机制
概念
当一个类加载器接受到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。
每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器。
作用
- 保证类加载的安全性:通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。
- 避免重复加载:双亲委派机制可以避免同一个类被多次加载
源码分析
getParent()
@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Check access to the parent class loader
// If the caller's class loader is same as this class loader,
// permission check is performed.
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
- 返回真实加载这个类/接口的加载器,返回null表示“启动类加载器”。
- 如果这里使用了安全管理器的话,并且”调用者的类加载器“或者”请求加载这个类的类加载器的祖先类加载器“不为空。那么这个方法就会去调用安全管理器的『checkPermission()』方法来去看是否能访问到这个类的加载器
loadClass(String name, boolean resolve)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}
- 该方法在一开始采用synchronized (getClassLoadingLock(name))来确保loadClass方法在多线程情况下,只能被加载一次。
- 调用findLoadedClass(String)方法来检查这个类是否已经被加载了。
- 调用父类加载器的loadClass方法。如果父类加载器是null,那么会调用启动类加载器。
- 调用findClass(String)方法来寻找类。(findClass方法推荐重写)
findClass(String) (推荐重写)
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
加载类的方式
使用Class.forName方法,使用当前类的类加载器去加载指定的类
ClassLoader classLoader = Demo1.class.getClassLoader();
获取到类加载器,通过类加载器的LoadClass方法指定某个类加载器加载。
Class<?> clazz = classLoader.loadClass("类");
打破双亲委派机制
使用自定义类加载器
案例
l一个tomcat程序中是可以运行多个web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。如果不打破双亲委派机制,当应用类加载器加载web应用1中的serlvet后,web应用2中相同限定名的servlet类就无法加载了。
解决方案
tomcat中使用自定义加载器来实现应用间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。
原理
在同一个java虚拟机中,只有相同类加载器+相同类限定名才会被认为是同一个类。
自定义类加载器的实现
- 先别写一个普通方法类,供类加载器加载。
package com;
public class TwoNum {
//返回两数之和
public int twoNum(Integer a,Integer b){
return a+b;
}
public static void main(String[] args) {
TwoNum twoNum=new TwoNum();
System.out.println(twoNum.twoNum(1,2));
}
}
- 自定义ClassLoader重写里面的findClass()方法。
package com;
import java.io.*;
public class MyClassLoader extends ClassLoader{
private String codePath;
public MyClassLoader(ClassLoader parent, String codePath) {
super(parent);
this.codePath = codePath;
}
public MyClassLoader( String codePath) {
this.codePath = codePath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis=null;
ByteArrayOutputStream bos=null;
codePath=codePath+name.replace(".", File.separator)+".class";
byte[] bytes=new byte[1024];
int line=0;
try{
//读取编译后的文件
bis=new BufferedInputStream(new FileInputStream(codePath));
bos=new ByteArrayOutputStream();
while((line= bis.read(bytes))!=-1){
bos.write(bytes,0,line);
}
bos.flush();
bytes=bos.toByteArray();
return defineClass(null,bytes,0,bytes.length);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
bis.close();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.findClass(name);
}
}
- 想要打破双亲委派机制,则需要重写loadClass方法
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t1 = System.nanoTime();
//如果包名是com开头的,调用自定义类的findClass方法,否则调用父类的loadClass方法
if(name.startsWith("com")){
c = this.findClass(name);
}else{
c=this.getParent().loadClass(name);
}
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
线程上下文类加载器
案例
JDBC中使用了DriverManager来管理项目中引入的不同的数据库的驱动,比如mysql和oracle驱动。
DriverManger属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。
SPI机制
- jdk内置的一种服务提供发现机制
- spi的工作原理
-
- 在classpath路径下的meta-inf/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。
- 使用ServiceLoader加载实现类
-
- spi中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。
public static<S> ServiceLoader<S> load(Class<S> service){
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service,c1);
}
jdk8及之前的类加载器
- jdk8及之前的版本中,扩展类加载器和应用加载器的源码位于rt.jar包中的sun.misc.Launcher.java中。
- jdk9引入了module的概念,类加载器在设计上发生了很多变化
-
- 启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中,java中的bootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
- 扩展类加载器被替换成了平台类加载器,平台类加载器遵循模块化的字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。