目录
defineClass(String, byte[], int, int)
SecureClassLoader与URLClassLoader
Class.forName()与ClassLoader.loadClass()
背景
阅读此文之前,可以看看我之前的文章JVM学习笔记上(概述-本地方法栈)中相关章节,这篇文章将会再扩展一些类加载器的内容,包括ClassLoader的源码解读、双亲委派的打破、热替换等
概述
ClassLoader是java的核心组件,所有的Class都是由ClassLoader加载的。ClassLoader负责通过各种方式将Class二进制数据读取到JVM中,并将其转换为一个与目标类对应的java.lang.Class对象实例,然后由JVM对其进行链接、初始化等操作。因此,ClassLoader在整个类加载阶段,只能影响到类的加载,而不能改变类的链接和初始化行为。至于它是否可以运行,则由执行引擎决定。
类加载的分类
显式加载与隐式加载:
- 显式加载指的是在代码中调用ClassLoader加载class对象的加载方式,比如Class.forName()或者getClass().getClassLoader().loadClass();
- 隐式加载指的由JVM加载类的加载方式,例如new一个对象等;
学习类加载器的必要性
避免在ClassNotFoundException或NoClassDefFoundException发生时手足无措、需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作、可以自定义类加载器来重新定义类的加载规则等
命名空间
- 类的唯一性:对于任意一个类,都需要由他的类加载器和这个类本身一起确定其在JVM中的唯一性。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
- 命名空间:每个类加载器都有自己的命名空间,命名空间由该加载器和所有的父加载器所加载的类组成
在同一个命名空间中,不会出现类的全限定名相同的两个类;但在不同的命名空间中,就有可能出现两个类的全限定名相同
类加载机制的基本特征
- 双亲委派模型:但不是所有的类加载都遵守这个模型。有时启动类加载器所加载的类型可能要加载用户代码,例如jdk内部的ServiceProvider/ServiceLoader机制,用户可以在标准api框架上提供自己的实现。jdk也需要提供默认的参考时限,例如java中JNDI、JDBC、文件系统、Cipher等都是利用所谓的上下文加载器而不是双亲委派模型去加载
- 可见性:子类加载器可以方位父加载器加载的类型,但反之不然,否则就缺少了必要的隔离,我们就不能利用类加载器实现容器的逻辑
- 单一性:由于父加载器加载的类型对于子加载器是可见的,因此父加载器加载过的类就不会在子加载器中再加载一遍。但是注意,类加载器邻居之间,同一个类仍然可以被加载多次,因为对等结点之间互相不可见
类的加载器分类
类加载器的分类:引导类(Bootstrap)加载器和自定义加载器(Extension、Application、自定义),前者由C/C++实现,后者由java实现
这四者的关系是包含关系,不是父类子类关系。子类加载器包含父加载器的引用
以sun.misc.Launcher(一个JVM的入口应用)为例,它里面类加载器类图如下所示
引导类加载器
由C/C++实现,嵌套在JVM内部,用来加载java的核心库(java、javax、sun开头的类)和扩展类以及应用程序类加载器,同时就是他们的父加载器,但自己没有父加载器
由于引导类加载器是由C实现的,所以java获取到的值为null,如下所示
classLoader = String.class.getClassLoader();
// 系统核心类库(String)使用引导类加载器加载
System.out.println("String的类加载器为:" + classLoader);
我们可以获取引导类加载器的加载路径,如下所示
// 获取引导类加载器能加载的路径
System.out.println("引导类加载器的加载路径:");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toString());
}
扩展类加载器
java实现,由sun.misc.Launcher$ExtClassLoader实现,继承于ClassLoader类,父加载器为启动类加载器。
我们可以获取扩展类加载器的加载路径,如下所示
System.out.println("扩展类加载器的加载路径");
String extDirs = System.getProperty("java.ext.dirs");
for (String s : extDirs.split(";")) {
System.out.println(s);
}
也就是ext目录下的类,如果我们把自己的jar包放到其下,也可以被扩展类加载器加载
系统类加载器
默认的系统类加载器就是应用类加载器,java实现,继承于ClassLoader,用于加载用户自定义类,如下所示
ClassLoader classLoader = StackStructTest.class.getClassLoader();
// 用户自定义类使用应用加载器加载
System.out.println("StackStructTest的类加载器为:" + classLoader);
用户自定义类加载器
通过用户自定义类加载器可以实现插件机制(OSGI、Eclipse的插件机制等,动态增加新功能)和应用隔离(Tomcat、Spring等中间件等,隔离不同的组件模块),通常要继承于ClassLoader
测试不同的类加载器
首先测试系统类加载器,并进行祖辈追溯
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent); // sun.misc.Launcher$ExtClassLoader@1b6d3586
ClassLoader bootstrap = parent.getParent();
System.out.println(bootstrap); // null,因为是C/C++实现的
然后是核心类和用户自定义类使用的加载器测试,上面第二节刚测过,此处略过
对于数组对象,主要看元素类型:如果是一般类,就用系统类加载器;核心类就用引导类加载器;扩展类就用扩展类加载器;基本数据类型就不用类加载器
String[] arr0 = new String[10];
System.out.println(arr0.getClass().getClassLoader()); // null
ClassLoaderTest1[] arr1 = new ClassLoaderTest1[10];
System.out.println(arr1.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
int[] arr2 = new int[10];
System.out.println(arr2.getClass().getClassLoader()); // null,基本数据类型数组不需要加载
ClassLoader源码解析
ClassLoader的主要方法
loadClass(String)
加载形参指定的类实例,用双亲委派机制实现,如果找不到类,就抛出ClassNotFoundException,源码如下
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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); // 如果父加载器为null,说明父加载器是引导类加载器,使用它查找形参指定的引导类
}
} 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); // 如果目标类不能被父加载器加载(也不是引导类),就自己查找它,调用findClass(String)方法
// 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) { // 此处实参resolve为false,即不去解析此类
resolveClass(c);
}
return c;
}
}
看来类的加载(不使用父类加载器或引导类加载器的话)要进一步追踪到findClass()方法,我们就进去看一下
findClass(String)
这个方法的实现在URLClassLoader类里,它是扩展类与系统类加载器的父类
因此我们看URLClassLoader的findClass()方法即可,源码如下
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
// 把类全限定名中的.改成/,并在最后加上.后缀名(com.Person -> com/Person.class)
Resource res = ucp.getResource(path, false); // ucp是URLClassPath类,使用它的getResource()方法获取class文件资源
if (res != null) {
try {
return defineClass(name, res); // 调用defineClass()方法,传入class文件资源和类全限定名
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
看来还要到defineClass()方法中一探究竟
defineClass(byte[], int, int)
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.'); // 类全限定名中最后一个.的索引
URL url = res.getCodeSourceURL(); // 字节码文件URL
if (i != -1) { // 如果最后一个.的索引不为-1,就是类全限定名中存在包名,所以需要对包进行加载
String pkgname = name.substring(0, i); // 解析包名
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url); // 加载包
}
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer(); // 获取字节码文件资源的ByteBuffer
if (bb != null) {
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners(); // 获取字节码资源的CodeSigners
CodeSource cs = new CodeSource(url, signers); // 构造CodeSource对象
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs); // 进一步加载类
} else {
byte[] b = res.getBytes(); // 获取不到字节码资源的ByteBuffer的话,就直接用它的bytes
// 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); // 用使用bytes的方式进一步加载类
}
}
使用ByteBuffer的defineClass()的追踪过程如下:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
CodeSource cs)
{
return defineClass(name, b, getProtectionDomain(cs));
// getProtectionDomain()方法把CodeSource对象和当前类加载器封装成受保护域对象,这个对象涉及java的沙箱安全机制
}
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining(); // 字节码资源字节缓冲的(剩余)长度
// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) { // 如果字节缓冲不是直接缓存的话,根据其是否存在数组,来给defineClass()传入不同的参数
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}
// 使用直接缓存的字节码流,就要直接使用本地方法来进行类的加载
protectionDomain = preDefineClass(name, protectionDomain); // 检查类名和包名的合法性,保护核心API
String source = defineClassSourceLocation(protectionDomain); // 获取字节码文件位置
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source); // 本地方法,追踪结束
postDefineClass(c, protectionDomain); // 善后处理
return c;
}
因此我们进一步追踪defineClass()方法
defineClass(String, byte[], int, int)
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain); // 检查类名和包名的合法性,保护核心API
String source = defineClassSourceLocation(protectionDomain); // 获取字节码文件位置
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); // 本地方法,追踪结束
postDefineClass(c, protectionDomain); // 善后处理
return c;
}
至此,类的加载就追踪结束了
resolveClass(Class<?>):
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class<?> c);
用来对类进行解析,没什么好追踪的
我们追踪类加载源码的意义,其实是为了发现findClass()与loadClass()的区别,显然loadClass()实现了双亲委派机制,并且调用了findClass(),因此我们实现自己的ClassLoader类时,覆写findClass()即可
SecureClassLoader与URLClassLoader
SecureClassLoader是ClassLoader的子类,URLClassLoader是SecureClassLoader的子类。
SecureClassLoader加入了几个对相关的代码源(CodeSource)和权限定义类进行验证的方法,主要对代码源的位置、证书和字节码访问权限进行验证,一般我们不会跟这个类打交道
URLClassLoader提供了findClass()、findResource()等方法的具体实现,并增加了URLClassPath类来获取字节码流。如果我们没有太复杂的需求,可以直接继承URLClassLoader类来实现自己的类加载器
ExtClassLoader与AppClassLoader
这两个都是URLClassLoader的子类,均为sun.misc.Launcher类的内部类。由于它俩都没有覆写loadClass()方法,因此都遵循双亲委派机制
Class.forName()与ClassLoader.loadClass()
Class.forName()是一个静态方法,根据传入的类全限定名获得一个Class对象。此方法在将字节码文件加载到内存的同时,会执行类的初始化;
ClassLoader.loadClass()是一个实例方法,需要一个ClassLoader对象来调用该方法。此方法将字节码文件加载到内存的同时不会执行类的初始化,直到这个类第一次被使用时才会进行初始化。
双亲委派模型
双亲委派模型从jdk1.2版本就开始出现,是为了保护java平台的安全
定义与本质
定义:如果一个类加载器在接到加载类的请求时,他首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归。如果父类加载器可以完成类加载任务,就成功返回;否则才自己加载(方才对loadClass()方法的源码解析也印证了这一点)
本质:规定了类加载器的使用顺序为引导类加载器->扩展类加载器->系统类或自定义类加载器
优势与劣势
优势:避免类的重复加载,保证了类的全局唯一性;保护程序安全防止核心api被篡改(defineClass()中调用的preDefineClass()方法)
劣势:根据辩证法,双亲委派机制也有自己的弊端,那就是顶层的类加载器不能访问底层类加载器加载的类,因为双亲委派机制是自底向上单向的。比如在引导类里提供一个接口,实现在应用类中,那么引导类加载的接口类就不能访问系统类加载器加载的实例。
JVM规范没有明确要求类加载器必须使用双亲委派模型,只是建议采用此方法而已。比如Tomcat的类加载器就反其道而行之,遇到一个类要加载时,要先自己加载,自己不能加载时才让父类加载器去加载。
三次破坏双亲委派模型的情况
第一次发生在双亲委派模型出现之前:由于双亲委派模型在jdk1.2才引入,但是类加载器java.lang.ClassLoader则在java第一个版本中就已然存在。面对已经存在的用户自定义类加载器时,java设计者们引入双亲委派模型时就不得不做一些妥协:在新的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写类加载逻辑时尽可能去覆写这个受保护的方法,而不是loadClass()
第二次是由这个模型自身的缺陷导致的:核心API不能调用用户类的代码。java设计者们为了解决这个问题,引入了一个不太优雅的设计——线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。如果创建线程时没有设置,它将会从父线程中继承一个。如果整个应用程序的全局范围内都没与设置线程上下文类加载器的话,默认使用的就是系统类加载器。这种类加载器的作用是:JNDI服务可以通过它加载所需的SPI(引导类提供、应用层实现的接口)代码。这是一种父类加载器请求子类加载器完成类加载的行为,这完全是对双亲委派机制的背道而驰,但也没有办法。java中涉及SPI的加载行为基本都是用这种方式完成,例如JDBC、JNDI、JCE、JAXB和JBI等。
第三次是由于用户对程序的动态性的追求导致的,例如代码热替换、模块热部署等:IBM公司主导的JSR-291(即OSGi R4.2)实现模块化热部署的关键是它自定义的类加载器的实现。每一个程序模块都有一个自己的类加载器,到需要更换一个模块时,就把模块连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再使用双亲委派模型的树状结构,而是进一步发展为更加复杂的网状结构。
当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
- 将以java.*开头或者在委派列表名单中的类,委派给父类加载器加载;
- 否则,将Import列表中的类,委派给Export这个类的模块类加载器加载;
- 否则,查找当前模块的ClassPath,使用自己的类加载器加载;
- 找不到的话,查找类是否在自己的Fragment模块中,是的话委派过去;
- 还是找不到,查找DynamicImport列表中的模块,委派给对应模块的的类加载器加载;
- 再不行就查找失败
这个步骤中,只有头一步符合双亲委派模型,剩下的都是再对等类加载器之间进行的
热替换的实现
热替换是指在程序运行过程中,不终止程序,只是通过替换程序文件来修改程序运行表现的行为,其关键在于在服务不中断的情况下,把修改表现在服务中。基本是大部分脚本语言天生支持热更新,例如php、bash等;但对于java来说,热替换就不是天生支持的了,因为一个类如果已经加载到系统中,再修改类文件,并不能让系统再去加载并重定义这个类。不过,由于不同的ClassLoader加载的同名类是不同的类,不能相互替换或兼容,因此我们可以利用这一点来模拟热替换的实现,思路如下
实现代码如下
public class HotSwap {
public static void main(String[] args) throws Exception {
while (true) {
String rootDir = "D:\\develop\\ideaWorkspace\\JVMDemo\\target\\classes\\";
MyClassLoader classLoader = new MyClassLoader(rootDir); // 定义class文件目录
Class clazz = classLoader.findClass("Demo"); // 调用覆写的findClass()方法加载类,直接不走双亲委派
Object demo = clazz.newInstance(); // 利用反射实现方法的热替换
Method method = clazz.getMethod("f");
method.invoke(demo);
Thread.sleep(500);
}
}
}
自定义类加载器代码如下
class MyClassLoader extends ClassLoader {
private String mRootDir; // 字节码文件目录路径
public MyClassLoader(String rootDir) {
mRootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = findLoadedClass(name);
if (clazz == null) { // 如果类没有被加载,就读取指定目录下的字节码文件,将其转换成字节数组,进行类的加载。
try {
String classFile = getClassFile(name);
FileInputStream inputStream = new FileInputStream(classFile);
FileChannel fileChannel = inputStream.getChannel();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
WritableByteChannel outChannel = Channels.newChannel(outputStream);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileChannel.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
byte[] bytes = outputStream.toByteArray();
clazz = defineClass(name, bytes, 0, bytes.length);
} catch (Exception ignored) {}
}
return clazz;
}
private String getClassFile(String name) { // 根据类名获取字节码文件的绝对路径
return mRootDir + "\\" + name.replace(".", "\\") + ".class";
}
测试的Demo类如下
public class Demo {
public void f() {
System.out.println("Old.....");
// System.out.println("New.....");
}
}
测试时,先运行一会儿,再注释掉Old输出语句,打开New输出语句的注释,然后重新编译Demo.java,就可以发现程序不终止就加载了新的字节码文件中的类
沙箱安全机制
java安全模型的核心就是沙箱,沙箱就是一个限制程序运行的环境,用来保证程序安全和保护java原生的jdk代码。沙箱安全机制将java代码限定在JVM特定的运行范围中,并且严格限制代码对本地系统资源的调用从而实现对代码的有效隔离,防止对本地系统造成破坏。
沙箱限制访问的系统资源有CPU、内存、文件系统和网络等,不同级别的沙箱对这些资源访问的限制也不一样,所有的Java程序都可指定沙箱来进行安全策略的定制。
在当前最新的沙箱安全机制中,引入了域(Domain)的概念。JVM会把所有代码加载到不同的系统域和应用域。系统域负责专门和关键资源进行交互,应用域则通过系统域的部分代理来对各种资源进行按需访问。JVM中不同的受保护域(ProtectedDomain)对应不同的权限,域中的类文件具有当前域中的全部权限,如下图所示
自定义类加载器的实现
好处
- 隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如阿里内某容器框架通过自定义类加载器确保应用中所依赖的jar包不会影响到中间件中的jar包;tomcat这种web应用服务器,内部也自定义了好几种类加载器,用于隔离同一个web应用服务器中的不同应用程序
- 修改类的加载方式:类的加载模型并非强制(引导类除外),别的加载类不一定非要引入,我们可以根据实际情况在某个时间点进行按需加载
- 扩展加载源:比如从数据库、网络,甚至机顶盒中进行加载
- 防止代码泄漏:java代码容易被变异和篡改,因此我们在加载类时可以对字节码进行加解密
场景
- 实现类似进程内隔离:类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰,典型的应用时javaEE、OSGI、JPMS等框架
- 应用需要从不同的数据源获取类定义信息,例如网络数据源。或者需要自己操纵字节码,动态修改或者生成类型
实现方式
- 覆写loadClass()方法:默认的loadClass()方法实现双亲委派机制,调用findClass()方法。擅自修改loadClass()方法会导致模型破坏,我们最好别这么做
- 覆写findClass()方法:加载字节码文件的字节数组,调用defineClass()方法,我们一般覆写这个方法
代码实现
public class MyClassLoader1 extends ClassLoader {
private String mDirPath;
public MyClassLoader1(String dirPath) {
mDirPath = dirPath;
}
public MyClassLoader1(ClassLoader parent, String dirPath) {
super(parent);
mDirPath = dirPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = findLoadedClass(name);
if (clazz == null) {
try {
// IO流获取字节码文件的字节数组
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(new File(getClassFilePath(name))));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
byte[] outBytes = outputStream.toByteArray();
outputStream.close();
inputStream.close();
// 调用defineClass()加载类
clazz = defineClass(name, outBytes, 0, outBytes.length);
} catch (Exception e) {
e.printStackTrace();
}
}
return clazz;
}
private String getClassFilePath(String name) {
return mDirPath + "/" + name.replace(".", "/") + ".class";
}
}
测试
public class Demo {
public void f() {
System.out.println("New.....");
}
public static void main(String[] args) throws Exception {
MyClassLoader1 loader = new MyClassLoader1("D:/");
Class<?> clazz = loader.loadClass("Demo");
Object o = clazz.newInstance();
Method method = clazz.getMethod("f");
method.invoke(o);
System.out.println(loader.getClass().getCanonicalName());
System.out.println(loader.getParent().getClass().getCanonicalName());
}
}
输出如下
jdk9新特性
为了保证兼容性,jdk9没有从根本上改变三层类加载器结构和双亲委派模型,但为了实现模块化,还是做了一些变动
1)、扩展机制被移除,扩展类加载器为了向后兼容被保留,只是被重命名为平台类加载器(Platform ClassLoader),可以通过ClassLoader的新方法getPlatformClassLoader()来获取。jdk9是基于模块化进行构建的,原来的rt.jar和tools.jar被拆分成几十个JMOD文件,其中的java类库已经天然满足了可扩展的需求,那自然无需保留lib/ext目录,此前使用这个目录或者java.ext.dirs环境变量来扩展jdk功能的机制也不必继续存在了。jdk9的模块化可以从我的笔记jdk9新特性中进行了解
2)、平台类加载器和应用程序类加载器不在继承自java.net.URLClassLoader,而是继承自jdk.internal.loader.BuiltinClassLoader
3)、类加载器有了名称,在构造方法中指定,可以通过getName()方法获取。平台类加载器的名称为platform,应用类加载器的名称为app
4)、启动类加载器现在是在JVM内部和java类库一起实现的类加载器,而不是以前的纯C++实现,但为了跟之前代码兼容,在获取启动类加载器时依旧会返回null
5)、类加载的委派关系也发生了变化,当平台及应用类加载器要加载某个类时,先判断类是否可以归属到某个系统模块中,如果可以就委派给负责那个模块的加载器去加载,不行再执行双亲委派