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

本文由读者 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 面试解析

扫描下面二维码付费阅读

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/546608.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql 数据库文件导入和导出、远程上传和下载数据库

1.打开数据库操作软件:Navicat Premium 15 中文版,登录测试连接! 2.打开本地mysql 数据库,新建数据库的名字必须和要导入的数据库名字一致。字符集和排 序规则按下图设置即可。

经典面试题 | 讲一下垃圾回收器都有哪些?

垃圾回收器有哪些&#xff1f;是一个高频的面试题&#xff0c;那本文就详细来解答这个问题。因为不同的厂商&#xff08;IBM、Oracle&#xff09;&#xff0c;实现的垃圾回收器各不相同&#xff0c;而本文要讨论的是 Oracle 的 HotSpot 虚拟机所使用的垃圾回收器。常用垃圾回收…

十、华为鸿蒙HarmonyOS应用开发之Java UI框架、常用TabList组件使用

一、TabList和Tab 二、TabList组件实例测试 1.新建基于empty ability(Java

dotConnect for Oracle控件免费下载及使用方法

原文来自龙博方案网http://www.fanganwang.com/product/1330转载请注明出处dotConnect for Oracle完全基于ADO.NET方法&#xff0c;因此您完全可以采用标准ADO.NET数据提供的方法来使用它。是一款为Microsoft .NET Framework提供直接Oracle数据库连接的数据发生器控件。 具体功…

面试官:谈谈数据库连接池的原理

来源&#xff1a;https://dwz.cn/oUF6pKOW这次我们采取技术演进的方式来谈谈数据库连接池的技术出现过程及其原理&#xff0c;以及当下最流行的开源数据库连接池jar包。一、早期我们怎么进行数据库操作1、原理一般来说&#xff0c;Java应用程序访问数据库的过程是&#xff1a;①…

php调用twitter api

今天做了一个关于调用twitter api来同步twitter信息到自己网站的功能&#xff0c;虽然是比较简单的功能&#xff0c;着实花费了我不少时间。网上有很多关于twitter api调用的方法&#xff0c;我试了几个都没有成功&#xff0c;貌似需要Oauth验证&#xff0c;这个跟微信开发类似…

8种常见SQL错误用法,你犯过几个?

来源&#xff1a;https://yq.aliyun.com/articles/72501高考和端午小长假一溜烟的功夫就全过完了&#xff0c;新的一周&#xff0c;让我们迎着清晨的阳光&#xff0c;继续前行。1、LIMIT 语句分页查询是最常用的场景之一&#xff0c;但也通常也是最容易出问题的地方。比如对于下…

51单片机 16*64LED单红点阵屏驱动测试,上位机改字软件免费版

public.h #ifndef __PUBLIC_H__ #define __PUBLIC_H__#include "STC12C5A60S2.h" #include <intrins.h> #include <string.h>//包含memset函数#define UCHAR unsigned char #define UINT unsigned int #define TRUE 1 #define FALSE 0#define LEDON 0…

cocos2dx基础篇(2)——Win32移植到Android

【安装工具】JDKEclipse 或直接下一个 ADT集成开发工具SDKNDK9rADTCDT安装方法自行百度。【正文】1、由 Cocos2D-X 和 VS2010 环境配置——1 中提到的python方法&#xff0c;创建新项目。然后将你开发好的win项目相应的文件Classes和Resources文件夹里的东西分别拷贝到刚创建的…

HR一般不会告诉你的八大真相

我们都是经历过招聘面试的&#xff0c;HR那种微笑很深奥很捉摸不透想必你也见过&#xff0c;但是就是猜不透是什么意思对吧&#xff1f;那么&#xff0c;就赶紧来看看这些HR一般不会告诉你的真相吧!1.求职实际上是一个难以公平的被拒绝过程公平的环境下求职?从HR的角度来说&am…

北京爱情故事-经典语录

2019独角兽企业重金招聘Python工程师标准>>> 小母牛追小公牛&#xff0c;牛逼急了&#xff01; 牛就一个字&#xff0c;我只说一次&#xff01; 男人在外面对女人有多坏&#xff0c;回家就对老婆有多好。——王学兵 结婚十年后&#xff0c;我终于发现其实人生上半…

memcache、redis原理对比

blog.csdn.net/suifeng3051/article/details/23739295一、问题&#xff1a;数据库表数据量极大&#xff08;千万条&#xff09;&#xff0c;要求让服务器更加快速地响应用户的需求。二、解决方案&#xff1a;1.通过高速服务器Cache缓存数据库数据2.内存数据库这里仅从数据缓存方…

我被面试官给虐懵了,竟然是因为我不懂Spring中的@Configuration

作者 l cxuan来源 l Hollis&#xff08;ID&#xff1a;hollischuang&#xff09;现在大部分的Spring项目都采用了基于注解的配置&#xff0c;采用了Configuration 替换标签的做法。一行简单的注解就可以解决很多事情。但是&#xff0c;其实每一个注解背后都有很多值得学习和思考…

jQuery中的.bind()、.live()和.delegate()之间区别分析,学习jquery

2019独角兽企业重金招聘Python工程师标准>>> DOM树 首先&#xff0c;可视化一个HMTL文档的DOM树是很有帮助的。一个简单的HTML页面看起来就像是这个样子&#xff1a; 事件冒泡(又称事件传播) 当我们点击一个链接时&#xff0c;其触发了链接元素的单击事件&#xff0…

Java中常用的4个Json库,哪个性能更牛逼?

来源&#xff1a;http://u6.gg/sDMab前言每次都在网上看到别人说什么某某库性能是如何如何的好&#xff0c;碾压其他的库。但是百闻不如一见&#xff0c;只有自己亲手测试过的才是最值得相信的&#xff0c;本篇通过JMH来测试一下Java中几种常见的JSON解析库的性能。JSON不管是在…

别在Java代码里乱打日志了,这才是正确的打日志姿势!

原文链接&#xff1a;http://t.cn/E9BkD7a使用slf4j使用门面模式的日志框架&#xff0c;有利于维护和各个类的日志处理方式统一。实现方式统一使用: Logback框架打日志的正确方式什么时候应该打日志当你遇到问题的时候&#xff0c;只能通过debug功能来确定问题&#xff0c;你应…

局域网IP地址查看软件V2.0使用教程 Python办公自动化

简介&#xff1a; 在企业、公司都有自己的局域网&#xff0c;作为维护网络运维人员还在使用纸质或电子excel表记录计算机IP信息。是不是很希望有一款工具软件能够管理局域网内的计算机IP地址&#xff0c;可以统计出不同VLAN下的已使用IP-MAC记录和未使用的IP地址&#xff1f;再…

Oracle 10g RAC 升级(CPU Update)之--升级CRS

Oracle 10g RAC 升级&#xff08;CPU Update&#xff09;之--升级CRS系统环境&#xff1a;操作系统&#xff1a;RedHat EL5Cluster&#xff1a; Oracle CRS 10.2.0.1.0Oracle&#xff1a; Oracle 10.2.0.1.0如图所示&#xff1a;RAC 系统架构案例&#xff1a;本案例是从 Oracl…

面试题:聊一聊设计模式的基本原则

设计模式&#xff08;Design pattern&#xff09;代表了程序开发的最佳实践&#xff0c;通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结…

python爬虫程序requests采用get和post方式

第1个爬虫程序&#xff1a; main.py #! python # -*- coding: utf-8 -*- from urllib.request import urlopen url "https://www.hao123.com/"if __name__ __main__:resp urlopen(url)# 把读取到网页的页面源代码写入myGetFile.html文件with open("myGetFil…