本文由读者 apdoer 投稿,apdoer 是一个极具钻研精神的 Java 猿,技术牛X头发茂盛!

博客地址:https://blog.csdn.net/m0_43452671

缘起:一个面试题

最近在上下班地铁刷博客,无意刷到一个面试题,号称很多程序员的烈士公墓:

java 能否自己写一个类叫 java.lang.System

博主也提供了相关的答案:

一般情况下是不可以的,但是可以通过特殊的处理来达到目的,这个特殊的处理就是自己写个类加载器来加载自己写的这个 java.lang.System 类。

然后随手又刷了几个,基本雷同,看到的博客都是在讲 java 类加载的双亲委托机制, 一个类在需要加载时,会向上委托,直到最上层的 bootstrapClassLoader ,然后最上层的 bootstrapClassLoader 如果能在自己对应的目录加载就加载,不能就向下查找。

而 bootstrapClassLoader 会加载系统默认的 System 类,所以我们自定义的就不会被加载。

但是我们自定义一个类加载器加载特定路径的,避开 jvm 默认的三个类加载器的加载路径,就可以使我们的自定义 System 类被加载。

可是真的是这样吗?

为了弄清楚这个问题,我又看了下类加载。

什么是类加载

  • 类加载指的是将类 Class 文件读入内存,并为之创建一个 java.lang.Class 对象, class 文件被载入到了内存之后,才能被其它 class 所引用

  • jvm 启动的时候,并不会一次性加载所有的 class 文件,而是根据需要去动态加载

  • java 类加载器是 jre 的一部分,负责动态加载 java 类到 java 虚拟机的内存

  • 类的唯一性由类加载器和类共同决定

还了解到系统的三种类加载器:

  • AppClassLoader : 也称为 SystemAppClass 加载当前应用的 classpath 的所有类。

  • ExtClassLoader : 扩展的类加载器,加载目录 %JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件。还可以加载 -D java.ext.dirs 选项指定的目录。

  • BoostrapClassLoader : 最顶层的加载类,主要加载核心类库, %JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class 等。另外需要注意的是可以通过启动 jvm 时指定 -Xbootclasspath 和路径来改变 Bootstrap ClassLoader 的加载目录。比如 java -Xbootclasspath/a:path 被指定的文件追加到默认的 bootstrap 路径中。

瞄一眼源码,在Launcher类中

public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();private static Launcher launcher = new Launcher();private static String bootClassPath = System.getProperty("sun.boot.class.path");private ClassLoader loader;private static URLStreamHandler fileHandler;public static Launcher getLauncher() {return launcher;}public Launcher() {// 创建ExtClassLoaderLauncher.ExtClassLoader var1;var1 = Launcher.ExtClassLoader.getExtClassLoader();//创建AppClassLoaderthis.loader = Launcher.AppClassLoader.getAppClassLoader(var1);//设置AppClassLoader为线程上下文类加载器Thread.currentThread().setContextClassLoader(this.loader);}public ClassLoader getClassLoader() {return this.loader;}public static URLClassPath getBootstrapClassPath() {return Launcher.BootClassPathHolder.bcp;}//AppClassLoaderstatic class AppClassLoader extends URLClassLoader {public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {final String var1 = System.getProperty("java.class.path");public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {}//ExtClassLoaderstatic class ExtClassLoader extends URLClassLoader {private static volatile Launcher.ExtClassLoader instance;public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {}//创建ExtClassLoaderprivate static Launcher.ExtClassLoader createExtClassLoader() throws IOException {}private static File[] getExtDirs() {String var0 = System.getProperty("java.ext.dirs");File[] var1;

这段源码有以下几点

  • Launcher 类在构造函数初始化了 ExtClassLoader 和 AppClassLoader 并设置 AppClassLoader 为线程上下文类加载器。

  • 代码里面没有告诉我们 BoostrapClassLoader 从哪里来的,但却为其指定了要加载 class 文件的路径 sun.boot.class.path 。

  • BoostrapClassLoader 是由 c++ 编写的,内嵌在 jvm 中,所以不能显示的看到他的存在【这个不是从源码中得到】。

实践出真知

我们通过代码来检验下上面的理论。

类加载器的父子关系

public class Test {public static void main(String[] args) {System.out.println(Test.class.getClassLoader());System.out.println(Test.class.getClassLoader().getParent());System.out.println(Test.class.getClassLoader().getParent().getParent());}
}

这段代码我们可以看到类加载器的父子关系, APPClassLoader->ExtClassLoader->BoostrapClassLoader , 但是 BoostrapClassLoader 无法显示的获取到,只能看到是个 null

源码中的路径到底加载哪些目录

  • sun.boot.class.path

public static void main(String[] args) {String property = System.getProperty("sun.boot.class.path");//BoostrapClassLoaderString[] split = property.split(";");Arrays.asList(split).forEach(s -> System.out.println(s));
}

可以看到是 jre/lib 目录下一些核心 jar

  • java.ext.dirs

public static void main(String[] args) {String property = System.getProperty("java.ext.dirs");//ExtClassLoaderString[] split = property.split(";");Arrays.asList(split).forEach(s -> System.out.println(s));
}

  • java.class.path

 public static void main(String[] args) {String property = System.getProperty("java.class.path");//AppClassLoaderString[] split = property.split(";");Arrays.asList(split).forEach(s -> System.out.println(s));
}

可以看到,各个加载器加载的对应路径和前面的介绍是吻合的

类加载的双亲委托机制

这里直接来一张图(processon 图库满了,这个先将就下):

如果看不太懂可以看下以下解释

  • 一个 class 文件发送请求加载,会先找到自定义的类加载器,当然这里没画出来。

  • APPClassLoader 得到加载器请求后,向上委托交给 ExtClassLoader , ExtClassLoader 同理会交给 BoostrapClassLoader ,这是向上委托方向

  • 最终到达 BoostrapClassLoader ,会先在缓存中找,没有就尝试在自己能加载的路径去加载,找不到就交给 ExtClassLoader ,同理一直到用户自定义的 ClassLoader ,这就是向下查找方向

  • 前面说的类的唯一性由类和类加载器共同决定, 这样保证了确保了类的唯一性。

弄清楚这些,我们可以开始验证自定义的类加载器是否可以加载我们自定义的这个System类了

自定义类加载器

  • 新建一个 MyClassLoader 继承 ClassLoader ,并重写 loadclass 方法

package org.apder;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{public MyClassLoader(){super(null);}@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {String className = null;if (name != null && !"".equals(name)){if (name.startsWith("java.lang")){className = new StringBuilder("/").append(name.replace('.','/')).append(".class").toString();}else {className = new StringBuffer(name.substring(name.lastIndexOf('.')+1)).append(".class").toString();}System.out.println(className);InputStream is = getClass().getResourceAsStream(className);System.out.println(is);if (is == null) return super.loadClass(name);byte[] bytes = new byte[is.available()];is.read(bytes);return defineClass(name,bytes,0,bytes.length);}return super.loadClass(name);}
}

这里的代码很容易看懂,就不赘述了。

  • 测试

由于 System 需要用于打印获取结果,这里就用同属 lang 包的 Long 类:

public class Long {public void testClassLoader(){System.out.println("自定义Long类被"+Long.class.getClassLoader()+"加载了");}public static void main(String[] args) {System.out.println("Long");}
}

运行自定义 Long 类中 main 方法 报错如下:

出错原因很简单,这个自定义的 Long 类申请加载后,会被委托到 BoostrapClassLoader,BoostrapClassLoader 会在向下查找的过程中找到 rt.jar 中的 java.lang.Long 类并加载,执行 main 方法时,找不到 main 方法,所以报找不到 main 方法。

public class MyLong {public void testClassLoader(){System.out.println("自定义Math类被"+MyLong.class.getClassLoader()+"加载了");}public static void main(String[] args) {System.out.println("mylong");}
}

我们再定义一个自定义的 java.lang.MyLong 类,执行 main 方法,报错如下

很明显的堆栈信息,禁止使用的包名 java.lang ,我们点进去 preDefineClass 看看:

private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){if (!checkName(name))throw new NoClassDefFoundError("IllegalName: " + name);if ((name != null) && name.startsWith("java.")) {throw new SecurityException("Prohibited package name: " + name.substring(0, name.lastIndexOf('.')));}if (pd == null) {pd = defaultDomain;}if (name != null) checkCerts(name, pd.getCodeSource());return pd;
}

可以看到,当如果类的全路径名以 java. 开头时,就会报错,看到这里,开头的答案你是否有了结果呢?

我们梳理一下过程,如果用自定义的类加载器加载我们自定义的类

  • 会调用自定义类加载器的 loadClass 方法。

  • 而我们自定义的 classLoader 必须继承 ClassLoader,loadClass 方法会调用父类的 defineClass 方法。

  • 而父类的这个 defineClass 是一个 final 方法,无法被重写

  • 所以自定义的 classLoader 是无论如何也不可能加载到以 java. 开头的类的。

到这里,最开始的问题已经有了答案。我们无法自定义一个叫 java.lang.System 的类。

思考

如果我把 MyLong 打成 jar 放到 BoostrapClassLoader 的加载路径呢?让 BoostrapclassLoader 去加载,具体操作如下,在 jdk 的 jre 目录下创建 classes 目录,然后把 MyLong.jar 复制进去,再通过 vmOptions 追加这个 classes 目录以使 BoostrapClassLoader 加载:

可以看到仍然加载不了,如果能加载,在控制台是会有 load 信息的,如果不是 java.lang.Long ,是可以跨过 APPClassLoader 和 ExtClassLoader 来让 boostraPClassloader 来加载的,这里就不演示了,操作很简单。

下面是vm参数

-Xbootclasspath/a:c:\classloader.jar -verbose

由一个面试题引起的类加载器思考,既然已经写到这里,干脆把线程上下文类加载器也一并学习了。

拓展线程上下文类加载器

为什么不和前面三种类加载器放在一起说呢,这个线程上下文类加载器只是一个概念,是一个成员变量,而前三种是确切存在的,是一个类,我们来看一下 Thread 的源码:

public
class Thread implements Runnable {private ClassLoader contextClassLoader;public void setContextClassLoader(ClassLoader cl) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("setContextClassLoader"));}contextClassLoader = cl;}@CallerSensitivepublic ClassLoader getContextClassLoader() {if (contextClassLoader == null)return null;SecurityManager sm = System.getSecurityManager();if (sm != null) {ClassLoader.checkClassLoaderPermission(contextClassLoader,Reflection.getCallerClass());}return contextClassLoader;}
}

特点

  • 线程上下文类加载器是一个成员变量,可以通过相应的方法来设置和获取。

  • 每个线程都有一个线程类加载器,默认是 AppClassLoader 。

  • 子线程默认使用父线程的 ClassLoader ,除非子线程通过上面的 setContextClassLoader 来设置。

测试

针对以上两点简单测试一下:

public class Test {public static void main(String[] args) {Thread thread = new Thread(()->{});System.out.println(thread.getContextClassLoader());thread.setContextClassLoader(Test.class.getClassLoader().getParent());System.out.println(thread.getContextClassLoader());}
}

public class Test {public static void main(String[] args) {Thread thread = new Thread(()->{});Thread.currentThread().setContextClassLoader(Test.class.getClassLoader().getParent());thread.setContextClassLoader(Test.class.getClassLoader().getParent());System.out.println(thread.getContextClassLoader());}
}

可以证明以上三点

总结

  • java 三种类加载器

  • 一条主线-----路径

  • 一个机制->双亲委托

  • 两个方向->向上委托,向下查找

好了,本文就先介绍到这里,有问题欢迎留言讨论。

【End】

查看更多面试题内容,请访问《Java最常见200+面试题全解析》,它包含的模块有:

  • Java、JVM 最常见面试题解析

  • Spring、Spring MVC、MyBatis、Hibernate 面试题解析

  • MySQL、Redis 面试题解析

  • RabbitMQ、Kafka、Zookeeper 面试解析

  • 微服务 Spring Boot、Spring Cloud 面试解析

扫描下面二维码付费阅读

关注下方二维码,订阅更多精彩内容。

转发朋友圈,是对我最大的支持。

面试干货 | Java 能否自定义一个类叫 java.lang.System?相关推荐

  1. vb6.0 定义一个公共类_纠正网上的错误:能不能自定义一个类叫java.lang.System/String?...

    前语:不要为了读文章而读文章,一定要带着问题来读文章,勤思考. 作者:一汪清水  来源:https://dwz.cn/i7Pf6VwZ 最近,学习了下java类加载相关的知识.然后看到网上有一道面试题 ...

  2. Java黑皮书课后题第9章:*9.5(使用GregorianCalendar类)Java API中有一个位于包java.util中的类GregorianCalendar

    Java黑皮书课后题第9章:*9.5(使用GregorianCalendar类)Java API中有一个位于包java.util中的类GregorianCalendar 题目 代码 题目 代码 imp ...

  3. java qq聊天界面_【附源码】用Java写了一个类QQ界面聊天小项目,可在线聊天!...

    原标题:[附源码]用Java写了一个类QQ界面聊天小项目,可在线聊天! 目录: 1.功能实现 2.模块划分 3.使用到知识 4.部分代码实现 5.运行例图 1.功能实现 1.修改功能(密码.昵称.个性 ...

  4. Java (1)写一个类,名为Animal, 该类有两个私有属性,name(代表动物的名字),和legs(代表动物的腿的条数);要求为两个私有属性提供public的访问方法。并提供两个重载的构造方法,

    Java (1)写一个类,名为Animal, 该类有两个私有属性,name(代表动物的名字),和legs(代表动物的腿的条数);要求为两个私有属性提供public的访问方法.并提供两个重载的构造方法, ...

  5. 在php中自定义一个类的关键字为( ),精读《未来简史》尔雅章节测验答案

    精读<未来简史>尔雅章节测验答案 更多相关问题 [单选题]当采用环刀法取样时,取样点应位于每层土的()深度处. A. 1/2 B. 1/3 C. 1/4 D. 2/3 [填空题]OSI/R ...

  6. java 委托_java 能不能自己写一个类叫 java.lang.System/String 正确答案

    来自:一汪清水 | 责编:乐乐 链接:blog.csdn.net/tang9140/article/details/42738433    正文    最近学习了下java类加载相关的知识.然后看到网 ...

  7. java 怎样卸载一个类_java 类型卸载问题

    1.自定义类加载器,可以将其设置为unreachable状态 再通过GC卸载掉类 2.使用系统类加载器,但是不能将其设置为unreacheable状态 由于不能直接将系统类加载器设置为unreacha ...

  8. java基础—自定义一个比较器,按照字符串的长度升序的方法来比较字符串进行储存(java集合三)

    自定义一个比较器,按照字符串的长度升序的方法来比较字符串进行储存 import java.util.Comparator; import java.util.Iterator; import java ...

  9. java基础—自定义一个比较器,对TreeSet 集合中的元素按指定方法来排序(java集合六)

    自定义一个比较器,对TreeSet 集合中的元素按指定方法来排序 import java.util.Comparator; import java.util.Iterator; import java ...

最新文章

  1. php获取文件名称和扩展名
  2. Vue使用vue-pull-refresh插件实现下拉刷新
  3. Apache的简单应用
  4. Django 的cookie 与 session组件
  5. 如何一站式快速构建企业全场景数据库管理平台?
  6. qt实现对话框选择文件路径并保存(简易版)
  7. 【转】C#字符串转换为日期
  8. SOME/IP报文格式-Payload
  9. 上传doc,pdf,ppt,png,jpg,html文件并解析内容
  10. mysql的delete语句使用exists删除数据走不通
  11. input[type='radio'] 自定义样式___通过label标签重置input[radio]样式
  12. 用pr做简单的相册视频
  13. 孟岩:区块链是一个与物理、互联网不同的平行世界,从互联网到区块链需要跨越5座桥
  14. python读取usb数据_PyUSB:从USB设备读取
  15. android ndk开发
  16. 中职学校计算机打字比赛方案,职业学校教务工作计划
  17. AutoCAD2011免费下载AutoCAD2011安装教程(中文)
  18. Linux开发板C语言实现LED闪烁
  19. python爬虫实战-爬取微信公众号所有历史文章 - (00) 概述
  20. 安川伺服调试的一些经验

热门文章

  1. Shell 中各种括号的作用
  2. Promise进阶——如何实现一个Promise库
  3. console 速查手册
  4. 发送邮件程序报错454 Authentication failed以及POP3和SMTP简介
  5. 二分查找和二叉查找树
  6. 用了Redis里面的map和set
  7. 多线程基础(二)pthread的了解
  8. 【HDOJ】4363 Draw and paint
  9. c++构造函数详解(转)
  10. 2010年终人生的思考