java加载机制_详解Java类加载机制
一:ClassLoader
从JVM结构图中可以看到,类加载器的作用是将Java类文件加载到Java虚拟机。
HotSpot JVM结构,图片来自Java Garbage Collection Basics
只有当类被加载进虚拟机内存,才能使用对应的类。
在Java中,类加载过程大概分为以下几步:通过全限类名获取类文件字节数组。可来自本地文件、jar包、网络等。
在方法区/元空间保存类的描述信息、静态属性。
在JVM堆中生成一个对应的java.lang.Class对象。
理解Java的类加载机制,对理解JVM有很大帮助。
二:Java默认的类加载器
Java默认提供三个类加载器,分别为:Bootstrap ClassLoader
Extension ClassLoader
App ClassLoader
Bootstrap ClassLoader 负责加载Java基础类,主要是 %JRE_HOME%/lib/ 目录下的rt.jar、resources.jar、charsets.jar等。
Extension ClassLoader 负责加载Java扩展类,主要是 %JRE_HOME%/lib/ext 目录下的jar。
App ClassLoader 负责加载当前应用的ClassPath中的所有类。
三个ClassLoader所负责加载的类,可以通过以下方式进行查看。public class ClassPath { public static void main(String[] args) {
System.out.println("Bootstrap ClassLoader path: ");
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println("----------------------------");
System.out.println("Extension ClassLoader path: ");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("----------------------------");
System.out.println("App ClassLoader path: ");
System.out.println(System.getProperty("java.class.path"));
System.out.println("----------------------------");
}
}
具体原因,在源码分析章节说明。
其中Bootstrap ClassLoader是JVM级别的,由C++撰写。
Extension ClassLoader和App ClassLoader都是Java类。
JVM启动Bootstrap ClassLoader,然后初始化sun.misc.Launcher。
接着,Launcher初始化Extension ClassLoader和App ClassLoader。
三:源码分析
sun.misc.Launcher类是Java程序的入口。
其构造器如下: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);
……
}
其中有两行比较重要的代码:Launcher.ExtClassLoader.getExtClassLoader();this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
第一行初始化了ExtClassLoader,但没有指定其parent。
一些文章表示ExtClassLoader的父加载器是Bootstrap ClassLoader,这个说法其实并不完全准确。
第二行初始化了AppClassLoader,指定ExtClassLoader作为其父加载器。并将AppClassLoader作为系统类加载器。
AppClassLoader将会成为自定义ClassLoader的默认父加载器。
具体逻辑可按照以下顺序查看源代码:Launcher类的getClassLoader()方法。
ClassLoader类的initSystemClassLoader()方法。
ClassLoader类的getSystemClassLoader()方法。
ClassLoader类的ClassLoader()方法。
其中getSystemClassLoader()方法的注释为:/**
* Returns the system class loader for delegation. This is the default
* delegation parent for new ClassLoader instances, and is
* typically the class loader used to start the application.
**/
ExtClassLoader和AppClassLoader都继承了URLClassLoader类。
URLClassLoader支持从文件目录和jar包加载class。
ExtClassLoader和AppClassLoader都调用了父类的构造函数。public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory)
URLClassLoader类中有个属性为ucp,表示该ClassLoader负责搜索的路径。
ExtClassLoader和AppClassLoader最大的不同,即它们负责的路径不同。/* The search path for classes and resources */private final URLClassPath ucp;
查看源码可得:
ExtClassLoader负责搜索的路径为:String var0 = System.getProperty("java.ext.dirs");
AppClassLoader负责搜索的路径为:String var1 = System.getProperty("java.class.path");
所以,上一节可以通过这两个方法获取不同ClassLoader所负责加载的目录。
此外,Bootstrap ClassLoader负责搜索的路径为:String bootClassPath = System.getProperty("sun.boot.class.path");
ClassLoader源码
ClassLoader是一个抽象类,几个主要的方法如下:defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。
findClass(String name)查找名称为name的类,返回的结果是java.lang.Class类的实例。
loadClass(String name)加载名称为name的类,返回的结果是java.lang.Class类的实例。
resolveClass(Class> c)链接指定的Java 类。
其中,loadClass方法是最常涉及的一个。
其代码如下: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;
}
}
该方法主要的步骤如下:指定全限类名进行加载,首先调用findLoadedClass(name)判断当前类加载器是否已经加载该类。
如果没有被加载。则判断当前ClassLoader的父加载器是否为null。如果不为null,则委托其父加载器进行加载。如果为null,则使用Bootstrap ClassLoader进行加载。
如果父加载器或Bootstrap ClassLoader都无法加载,则调用findClass(name)方法寻找需要加载的类。
此外,loadClass方法还涉及加锁的过程,使用ConcurrentHashMap对不同的全限类名进行加锁。
具体可查看getClassLoadingLock方法。
四:双亲委托模式
Java类加载机制使用双亲委托模式。
一个ClassLoader加载一个类时,首先需要将任务委托给其父加载器,直到Bootstrap ClassLoader。
如果父加载器未加载该类,则逐层返回给委托发起者即当前ClassLoader进行加载。
在正常应用中,用户不自定义类加载器。
类加载工作首先由App ClassLoader发起,然后委托给Extension ClassLoader,最后委托给Bootstrap ClassLoader。
首先,通过一个例子了解三个ClassLoader所负责加载的类和双亲委托模式。
新建一个jar包,名为acai-cl.jar,包中有个简单的Person类。
写一个简单的程序输出person对象所对应的ClassLoader。import com.acai.Person;public class TestClassLoader { public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
}
}
测试一:将jar包引入项目
jar包引入项目
对应输出:sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到,位于ClassPath的类,是由App ClassLoader负责加载。
测试二:将jar包复制到%JRE_HOME%/lib/ext目录
复制到%JRE_HOME%/lib/ext
对应输出:sun.misc.Launcher$ExtClassLoader@4cc77c2e
可以得出,Extension ClassLoader负责加载%JRE_HOME%/lib/ext目录下的类。
加载Person类时,会首先尝试使用App ClassLoader进行加载。
由于双亲委托模式,最终委托到Extension ClassLoader,而其负责的目录%JRE_HOME%/lib/ext下存在Person类,则进行了类加载操作。
测试三:将jar包追加到Bootstrap ClassLoader加载路径上
追加到Bootstrap ClassLoader加载路径
使用参数:-Xbootclasspath/a:d:\acai-cl.jar,将jar包追加到Bootstrap ClassLoader加载路径。
对应输出:null
可以看出,Person类的加载工作,最终被委托到了Bootstrap ClassLoader。
注:Bootstrap ClassLoader由C++撰写。由Bootstrap ClassLoader负责加载的类,其getClassLoader()方法输出为null。
可以尝试输出String类的类加载器。System.out.println(String.class.getClassLoader());
接下来,再通过debug来验证双亲委托模式。
还是原来那个简单的demo。import com.acai.Person;public class Test { public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
}
}
在ClassLoader类的loadClass方法上打断点。
App ClassLoader尝试加载
Extension ClassLoader尝试加载
Bootstrap ClassLoader尝试加载
可以看出,类的加载过程符合从下到上委托,最终会被委托到Bootstrap ClassLoader。
同时符合从上到下加载,每一层ClassLoader都会尝试进行加载。最终由App ClassLoader加载了Person类。
接着,尝试加载一个特殊的类:Splash.class。
Splash类位于jfxrt.jar,这个jar包在%JRE_HOME%/lib/ext目录下。import com.sun.javafx.applet.Splash;public class ExtTest { public static void main(String[] args) {
Splash splash = new Splash(null);
System.out.println(splash.getClass().getClassLoader());
}
}
对应输出:sun.misc.Launcher$ExtClassLoader@330bedb4
毫无疑问,Splash类应该由Extension ClassLoader进行加载。
但其加载过程,仍然会从默认的系统类加载器App ClassLoader开始。
可以通过debug进行查看。
App ClassLoader尝试加载
Splash类加载的过程会被委托到Bootstrap ClassLoader,但Bootstrap ClassLoader并不负责加载%JRE_HOME%/lib/ext目录下的类。最终由Extension ClassLoader进行加载。
Bootstrap ClassLoader尝试加载未成功
最终由Extension ClassLoader加载
很多文章在阐述三个ClassLoader之间的关系时候,会给出一个getParent操作的demo。
并且认为Bootstrap ClassLoader是Extension ClassLoader的父加载器。
Extension ClassLoader是App ClassLoader的父加载器。
App ClassLoader是自定义类加载器的父加载器。
这样的解释基本正确,但Bootstrap ClassLoader和Extension ClassLoader之间的关系需要额外解释。
双亲委托机制,图片来自参考7
由于Bootstrap ClassLoader并不是使用Java编写,故无法指定Extension ClassLoader的parent为Bootstrap ClassLoader。
这一层关系在ClassLoader的loadClass方法中做了弥补。
在加载类时,会判断当前ClassLoader的父加载器是否为null,为null则使用Bootstrap ClassLoader进行加载。
在Java提供的三个默认类加载器中,父加载器为null的只有Extension ClassLoader。
该过程可参考ClassLoader的loadClass方法。
为什么使用双亲委托模式?
网上很多例子是关于String类。假设自己写一个java.lang.String类,使用双亲委托模式可以防止这个问题。
但其实双亲委托模式可以被打破,而真正阻止自定义java.lang.String的是“安全机制”。
这里尝试自定义java.lang.String类,并使用自定义ClassLoader进行加载。package java.lang;public class String {
}import java.io.IOException;import java.nio.file.Files;import java.nio.file.Paths;public class StringClassLoader extends ClassLoader { @Override
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { if ("java.lang.String".equals(name)) { return findClass(name);
} else { return super.loadClass(name);
}
} @Override
public Class> findClass(String s) throws ClassNotFoundException { try { byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class")); return defineClass(s, classBytes, 0, classBytes.length);
} catch (IOException e) { throw new ClassNotFoundException(s);
}
} public static void main(String[] args) throws ClassNotFoundException {
StringClassLoader stringClassLoader = new StringClassLoader();
Class clazz = stringClassLoader.loadClass("java.lang.String", false);
System.out.println(clazz.getClassLoader());
}
}
该自定义类加载器破坏了双亲委托机制,具体方式将在下个章节说明。
输出结果为:Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
可以看到,在被findClass方法调用的defineClass中有这么一段:if ((name != null) && name.startsWith("java.")) { throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
它会检查当前加载类的全限类名是否以java.开头,这也是一种安全机制。
如果按照网上的说法,java.lang.String被Bootstrap ClassLoader加载,demo中自定义的类加载器会被略过,不会输出异常。
所以说,双亲委托模式的作用只是防止类重复加载。
五:自定义ClassLoader
多数情况下,Java默认的三个类加载器已经可以满足需求。
自定义类加载器则可以实现额外的需求,例如:从网络文件加载类。
从任意目录加载类。
对字节码文件做加密处理,由自定义类加载器做解密。
实现自定义类加载器的主要步骤为:继承ClassLoader类。如果只是从目录或者jar包加载类,也可以选择继承URLClassLoader类。
重写findClass方法。
在重写的findClass方法中,无论用何种方法,获取类文件对应的字节数组,然后调用defineClass方法转换成类实例。
自定义类加载器真正好玩的是打破双亲委托机制,也是很多面试官会问到的问题。
上文提到类加载双亲委托模式实现位于ClassLoader的loadClass方法,想要破坏这个机制,则需要重写该方法。
打破双亲委托模式的确有一定的实用价值。
比如有两个class文件,或者两个jar包。
其中两个类的全限类名都一样,如果需要同时使用这两个类,则需要打破双亲委托模式。
有两个Person类,它们的全限类名均为com.acai.Person,唯一的区别是sayHello()方法输出的内容略有不同。package com.acai;import lombok.Data;@Datapublic class Person { private String name; private Integer age; public void sayHello() {
System.out.println("Hello, this is Person in acai-cl");
}
}package com.acai;import lombok.Data;@Datapublic class Person { private String name; private Integer age; public void sayHello() {
System.out.println("Hello, this is Person in acai-cl2");
}
}
将两个Person所在的项目打成jar包。
两个jar包
常规操作是,把两个jar包都引进项目。
写一个小小的demo。import com.acai.Person;public class Main { public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
}
}
对应输出为:sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl
可以看到,demo中默认使用了acai-cl.jar中的Person类。
如果想要使用acai-cl2.jar中的Person类,则想到新建一个ClassLoader。
需要从jar包加载类,则优先想到URLClassLoader。import com.acai.Person;import java.io.File;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;public class Main { public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
URL url = new File("d:/acai-cl2.jar").toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Thread.currentThread().setContextClassLoader(loader);
Class> clazz = loader.loadClass("com.acai.Person");
System.out.println(clazz.getClassLoader());
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(clazz.newInstance());
}
}
作者:阿菜的博客
链接:https://www.jianshu.com/p/d98324f5ad23
java加载机制_详解Java类加载机制相关推荐
- java双缓存机制_详解JVM类加载机制及类缓存问题的处理方法
前言 大家应该都知道,当一个Java项目启动的时候,JVM会找到main方法,根据对象之间的调用来对class文件和所引用的jar包中的class文件进行加载(其步骤分为加载.验证.准备.解析.初始化 ...
- java 配置文件的路径_详解java配置文件的路径问题
详解java配置文件的路径问题 详解java配置文件的路径问题 各种语言都有自己所支持的配置文件,配置文件中有很多变量是经常改变的.不将程序中的各种变量写死,这样能更方便地脱离程序本身去修改相关变量设 ...
- java同步异步调用_详解java 三种调用机制(同步、回调、异步)
1:同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,jsPwwCe它是一种单向调用 2:回调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口: 3:异步调用:一种类似消 ...
- java 死锁 内存消耗_详解Java中synchronized关键字的死锁和内存占用问题
先看一段synchronized 的详解: synchronized 是 java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并 ...
- java 配置文件配置路径_详解java配置文件的路径问题
详解java配置文件的路径问题 各种语言都有自己所支持的配置文件,配置文件中有很多变量是经常改变的.不将程序中的各种变量写死,这样能更方便地脱离程序本身去修改相关变量设置. 那么我们需要读取配置文件, ...
- java使用集合存储过程_详解java调用存储过程并封装成map
详解java调用存储过程并封装成map 发布于 2020-5-1| 复制链接 摘记: 详解java调用存储过程并封装成map 本文代码中注释写的比较清楚不在单独说明,希望能帮助到大 ...
- java 线程一直运行状态_详解JAVA 线程-线程的状态有哪些?它是如何工作的?
线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在. 一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源.更加轻量化,也因 ...
- java注解 源码_详解Java注解教程及自定义注解
详解Java注解教程及自定义注解 更新时间:2016-02-26 11:47:06 作者:佚名 我要评论(0) Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个 ...
- java的注解方式_详解Java注解的实现与使用方法
详解Java注解的实现与使用方法 Java注解是java5版本发布的,其作用就是节省配置文件,增强代码可读性.在如今各种框架及开发中非常常见,特此说明一下. 如何创建一个注解 每一个自定义的注解都由四 ...
最新文章
- Windows下C 用 Socket 发送图片--基础
- Android中RatingBar的自定义效果
- This tutorial code needs the xfeatures2d contrib module to be run.
- VS中一些提高编码效率的快捷键
- Android开发的环境搭建及HelloWorld的实现
- 【实习项目记录】(四)Android 实现手机验证时,按钮倒计时60s
- AngularJS-liveRoomDirective.js 直播间指令
- 如何训练您的医生...使用开源
- sql 截取字符串:
- layim mysql_ichat系统说明 · ThinkPHP5+workerman+layIM打造聊天系统 · 看云
- linux 多线程服务端编程 pdf,Linux 多线程服务端编程.pdf
- xshell5 的账号密码搬家
- 微信卡券的创建、领取、核销
- python-selenium 自动化弹幕
- 20中氨基酸名称、简写及化学式
- 深度学习之CNN宫颈癌预测
- 基于蚁群算法的图像边缘检测
- 【小程序】解析二维码decodeURIComponent()
- SQL Server阻塞与锁
- Ali-Perseus(擎天):统一深度学习分布式通信框架 [弹性人工智能]...
热门文章
- .class 字节码文件与Java RTTI(类型信息)(.class 类对象)
- Python、Java 在线笔试
- Python 基础 —— sorted
- SQOOP --hive-import 错误(Sqoop Hive exited with status 1)及解决
- 编程规范 —— 变量的命名
- python就业方向-为什么这么多人喜欢Python?Python的就业方向是什么?
- 自学python需要安装什么-学习python需要什么基础吗?老男孩Python
- python编程案例教程-Python程序设计案例教程——从入门到机器学习(微课版)
- python常用代码总结-Python基础常见问题总结(一)
- 在线语音识别_腾讯云在线语音识别_在线语音识别成文字 - 云+社区 - 腾讯云