深入理解Java类加载器(一):Java类加载原理解析续

SecurityManager sm = System.getSecurityManager();

if (sm != null) {

checkClassLoaderPermission(scl, Reflection.getCallerClass());

}

return scl;

}

我们再来看一下对应的getSystemClassLoader()方法的实现:

private static synchronized void initSystemClassLoader() {

//…

sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

scl = l.getClassLoader();

//…

}

我们可以写简单的测试代码来测试一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

输出的结果为:

sun.misc.Launcher$AppClassLoader@18b4aac2

所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

  • <Java_Runtime_Home>/lib下的类;

  • <Java_Runtime_Home>/lib/ext下或者由系统变量java.ext.dir指定位置中的类;

  • 当前工程类路径下或者由系统变量java.class.path指定位置中的类。

4、在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<JAVA_HOME>/lib下的类,但此时就不能够加载<JAVA_HOME>/lib/ext目录下的类了。

Ps:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。

5、编写自定义类加载器时,一般有哪些注意点?

1)、一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑(Old Generation)

一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)

public class WrongClassLoader extends ClassLoader {

public Class<?> loadClass(String name) throws ClassNotFoundException {

return this.findClass(name);

}

protected Class<?> findClass(String name) throws ClassNotFoundException {

// 假设此处只是到工程以外的特定目录D:\library下去加载类

// 具体实现代码省略

}

}

通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4中的结论就不成立了。大家可以简单测试一下,现在<JAVA_HOME>/lib、<JAVA_HOME>/lib/ext 和 工程类路径上的类都加载不上了。

//问题5测试代码一

public class WrongClassLoaderTest {

publicstaticvoid main(String[] args) {

try {

WrongClassLoader loader = new WrongClassLoader();

Class classLoaded = loader.loadClass(“beans.Account”);

System.out.println(classLoaded.getName());

System.out.println(classLoaded.getClassLoader());

} catch (Exception e) {

e.printStackTrace();

}

}

}/* Output:

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)

at java.io.FileInputStream.open(Native Method)

at java.io.FileInputStream.(FileInputStream.java:106)

at WrongClassLoader.findClass(WrongClassLoader.java:40)

at WrongClassLoader.loadClass(WrongClassLoader.java:29)

at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)

at java.lang.ClassLoader.defineClass1(Native Method)

at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

at WrongClassLoader.findClass(WrongClassLoader.java:43)

at WrongClassLoader.loadClass(WrongClassLoader.java:29)

at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

Exception in thread “main” java.lang.NoClassDefFoundError: java/lang/Object

at java.lang.ClassLoader.defineClass1(Native Method)

at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

at java.lang.ClassLoader.defineClass(ClassLoader.java:400)

at WrongClassLoader.findClass(WrongClassLoader.java:43)

at WrongClassLoader.loadClass(WrongClassLoader.java:29)

at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

*///:~

注意,这里D:”classes”beans”Account.class是物理存在的。这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

//问题5测试二

//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)

public class WrongClassLoader extends ClassLoader {

protected Class<?> findClass(String name) throws ClassNotFoundException {

//假设此处只是到工程以外的特定目录D:\library下去加载类

//具体实现代码省略

}

}/* Output:

beans.Account

WrongClassLoader@1c78e57

*///:~

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出正确。

2). 正确设置父类加载器

通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

3). 保证findClass(String name)方法的逻辑正确性

事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

6、如何在运行时判断系统类加载器能加载哪些路径下的类?

一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty(“java.class.path”)。如下所示:

package com.code.selfclassload;

import com.google.gson.Gson;

public class Question6Test

{

public static void main(String[] args)

{

System.out.println(“Rico”);

Gson gson = new Gson();

System.out.println(gson.getClass().getClassLoader());

System.out.println(System.getProperty(“java.class.path”));

}

}

在这里插入图片描述

如上述程序所示,Test类和Gson类由系统类加载器加载,并且其加载路径就是用户类路径,包括当前类路径和引用的第三方类库的路径。

7、如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

判断方法:

package com.code.selfclassload;

import java.net.URL;

import java.net.URLClassLoader;

public class ExtClassLoaderTest

{

public static void main(String[] args)

{

try {

URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();

for (int i = 0; i < extURLs.length; i++) {

System.out.println(extURLs[i]);

}

} catch (Exception e) {

//…

}

}

}

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/access-bridge-64.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/cldrdata.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/dnsns.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/jaccess.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/jfxrt.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/localedata.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/nashorn.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunec.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunjce_provider.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunmscapi.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunpkcs11.jar

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/zipfs.jar

五. 开发自己的类加载器

=============================================================================

在前面介绍类加载器的代理委派模型的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

package com.code.selfclassload;

import java.io.ByteArrayOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStream;

// 文件系统类加载器

public class FileSystemClassLoader extends ClassLoader

{

private String rootDir;

public FileSystemClassLoader(String rootDir) {

this.rootDir = rootDir;

}

// 获取类的字节码

@Override

protected Class<?> findClass(String name) throws ClassNotFoundException {

byte[] classData = getClassData(name); // 获取类的字节数组

if (classData == null) {

throw new ClassNotFoundException();

} else {

return defineClass(name, classData, 0, classData.length);

}

}

private byte[] getClassData(String className) {

// 读取类文件的字节

String path = classNameToPath(className);

try {

InputStream ins = new FileInputStream(path);

ByteArrayOutputStream baos = new ByteArrayOutputStream();

int bufferSize = 4096;

byte[] buffer = new byte[bufferSize];

int bytesNumRead = 0;

// 读取类文件的字节码

while ((bytesNumRead = ins.read(buffer)) != -1) {

baos.write(buffer, 0, bytesNumRead);

}

return baos.toByteArray();

} catch (IOException e) {

e.printStackTrace();

}

return null;

}

private String classNameToPath(String className) {

// 得到类文件的完全路径

return rootDir + File.separatorChar

  • className.replace(‘.’, File.separatorChar) + “.class”;

}

}

如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。加载本地文件系统上的类,示例如下:

package com.code.selfclassload;

public class Simple

{

private Simple instance;

public void setSample(Object instance) {

System.out.println(instance.toString());

this.instance = (Simple) instance;

}

}

=================================================================================

package com.code.selfclassload;

import java.lang.reflect.Method;

public class ClassIdentity

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

互联网大厂比较喜欢的人才特点:对技术有热情,强硬的技术基础实力;主动,善于团队协作,善于总结思考。无论是哪家公司,都很重视高并发高可用技术,重视基础,所以千万别小看任何知识。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。

**另外本人还整理收藏了2021年多家公司面试知识点以及各种技术点整理 **

下面有部分截图希望能对大家有所帮助。

在这里插入图片描述
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
取!!(备注Java获取)**

img

总结

互联网大厂比较喜欢的人才特点:对技术有热情,强硬的技术基础实力;主动,善于团队协作,善于总结思考。无论是哪家公司,都很重视高并发高可用技术,重视基础,所以千万别小看任何知识。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。

**另外本人还整理收藏了2021年多家公司面试知识点以及各种技术点整理 **

下面有部分截图希望能对大家有所帮助。

[外链图片转存中…(img-UTdic0Ya-1713748503341)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值