前言

曾经有一次,面试官问到类加载机制,相信大多数小伙伴都可以答上来双亲委派机制,也都知道JVM出于安全性的考虑,全限定类名相同的String是不能被加载的。但是如果加载了,会出现什么样的结果呢?异常?那是什么样的异常。如果包名不相同呢?自定义类加载器是否可以加载呢?相信面试官从各种不同的角度出击,很快就会答出漏洞,毕竟咱没有深入研究过虚拟机...

接下来笔者就针对上述问题进行一一验证。该篇文章抱着求证答案的方向出发,并无太多理论方面的详解。如有理解上的偏差,还望大家不吝赐教。

JVM都有哪些类加载器

首先我们放上一张节选自网络的JVM类加载机制示意图

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader:

  • BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。

  • ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。

  • AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

JVM类加载方式

类加载有三种方式:

  • 1、命令行启动应用时候由JVM初始化加载
  • 2、通过Class.forName()方法动态加载
  • 3、通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

JVM类加载机制源码

双亲委派模型实现源码分析

private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
} if (c == null) {
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;
}
}

双亲委派模型的好处

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

如果我们不想用双亲委派模型怎么办?

为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 loadClass() 即可。

系统类加载器加载自定义String

1. 首先我们看下普通的类加载过程

package com.example.demojava.loadclass;
public class ClassLoaderDemo{ public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}

结果输出

ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@75bd9247
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父类加载器为ExtClassLoader

ExtClassLoader的父类加载器为null,null并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader

2. 我们自己定义一个String类,看下会发生什么

package com.example.demojava.loadclass;

public class String {
public static void main(String[] args) {
System.out.println("我是自定义的String");
}
}

结果输出

➜  demo-java javac src/main/java/com/example/demojava/loadclass/String.java
➜ demo-java java src.main.java.com.example.demojava.loadclass.String
错误: 找不到或无法加载主类 src.main.java.com.example.demojava.loadclass.String

这里分明有main方法,全限定类名又和jdk的String不在同一个package(不会造成冲突),为什么会输出找不到或无法加载主类呢?

细心的小伙伴一定会发现该类'没有导入'系统的String类,会不会因为JVM的类加载机制,AppClassLoader加载类的时候,由于自定义的String被加载,拦截了上层的String类呢?String对象是自定义的,不符合main()方法的定义方式,故系统抛找不到main()方法。

我们反过来验证下刚才的推测,再次运行刚才的ClassLoaderDemo会发生什么呢?what?IDE中的main()方法去哪里了?还是手动编译运行下吧

➜  demo-java javac src/main/java/com/example/demojava/loadclass/ClassLoaderDemo.java
➜ demo-java java src.main.java.com.example.demojava.loadclass.ClassLoaderDemo
错误: 找不到或无法加载主类 src.main.java.com.example.demojava.loadclass.ClassLoaderDemo

结果显示: 之前正常运行的java类也找不到主类了。

我们导入正确的String类再来验证下

package com.example.demojava.loadclass;

public class String {
public static void main(java.lang.String[] args) {
System.out.println("我是自定义的String");
}
}

结果输出

我是自定义的String

3. 能否覆写lang包下的String类?

上边的案例修改包路径即可

package java.lang;

public class String {
public static void main(java.lang.String[] args) {
System.out.println("我是自定义的String");
}
}

输出报错

Connected to the target VM, address: '127.0.0.1:63569', transport: 'socket'
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

分析:首先由于全限定类名java.lang.String等于jdk中的String类,根据上边类加载源码可知,当AppClassLoader加载该String时,判断java.lang.String已经加载,便不会再次加载。所以执行的依旧是jdk中的String,但是系统的java.lang.String中没有main()方法,所以会报错。这是一种安全机制。

然后验证下默认的类加载器能否加载自定义的java.lang.String。==,默认的AppClassLoader能加载Everything?

public class LoadStringDemo {

    public static void main(String[] args) {
URLClassLoader systemClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
URL[] urLs = systemClassLoader.getURLs();
for (URL url: urLs) {
System.out.println(url);
}
}
}

输出日志如下

...
file:/Users/cuishiying/work/demo-java/target/classes/
...

日志太多,但是绝对没有其他的包路径(当前包下的java.lang.String默认只能时jdk中的)

自定义类加载器

为什么会存在自定义类加载器呢

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。

因为实际项目中,会有多种加载.class文件的方式,

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

如何自定义类加载器

package com.example.demojava.loadclass;

import com.demo.ClassLoaderDemo;

import java.io.*;
import java.lang.reflect.Method; public class MyClassLoader extends ClassLoader { private String root; /**
* @param name 全限定类名
* @return
* @throws ClassNotFoundException
*/ @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name); if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
} private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar +
className.replace('.', File.separatorChar) + ".class"; try {
InputStream ins = new FileInputStream(fileName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = 0; while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
} return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} return null;
} public String getRoot() {
return root;
} public void setRoot(String root) {
this.root = root;
} public static void main(String[] args) throws Exception { MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/cuishiying/Desktop/demo"); Class<?> clz = Class.forName("LoadDemo", true, classLoader);
Object instance = clz.newInstance();
Method test = clz.getDeclaredMethod("test");
test.setAccessible(true);
test.invoke(instance); System.out.println(instance.getClass().getClassLoader()); }
}

结果输出

test
com.example.demojava.loadclass.MyClassLoader@75bd9247

由此可知,自定义类加载器已可以正常工作。这里我们不能把LoadDemo放在类路径下,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader加载,而不会通过我们自定义类加载器来加载。

自定义类加载器加载手写java.lang.String

改写自定义类加载器的main()方法

    public static void main(String[] args) throws Exception {

        MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/cuishiying/Desktop/demo"); Class<?> clz = classLoader.findClass("java.lang.String");
Object instance = clz.newInstance(); System.out.println(instance.getClass().getClassLoader());
}

JVM由于安全机制抛出了SecurityException

/Users/cuishiying/Desktop/demo/java/lang/String.class
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.example.demojava.loadclass.MyClassLoader.findClass(MyClassLoader.java:25)
at com.example.demojava.loadclass.MyClassLoader.main(MyClassLoader.java:71)

公众号 【当我遇上你】

JVM类加载器是否可以加载自定义的String的更多相关文章

  1. JVM类加载(4)—加载器

    定义: 虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这个动作的代码模块称之为“类加载器 ...

  2. JVM类加载(1)—加载

    1.加载: 查找并加载类,将.class文件中的二进制数据读入到内存中,放在运行时数据区的方法内,然后在堆内创建一个java.lang.class对象用来封装类在方法区内的数据结构.并且向程序员提供了 ...

  3. JVM核心机制(类加载器、三种类加载器、代理加载模式、双亲委派机制

  4. A3. JVM 类加载器

    [概述] 虚拟机设计团队把类加载阶段中的 “通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这个动作的代码 ...

  5. JVM 类加载器命名空间深度解析与实例分析

    一.创建Sample 1.创建实例 public class MyPerson { private MyPerson myPerson; public void setMyPerson(Object ...

  6. JVM 类加载器ClassLoader源码学习笔记

    类加载 在Java代码中,类型的加载.连接与初始化过程都是在程序运行期间完成的. 类型可以是Class,Interface, 枚举等. Java虚拟机与程序的生命周期 在如下几种情况下,Java虚拟机 ...

  7. 五、JVM — 类加载器

    回顾一下类加载过程 类加载器总结 双亲委派模型 双亲委派模型介绍 双亲委派模型实现源码分析 双亲委派模型的好处 如果我们不想要双亲委派模型怎么办? 自定义类加载器 推荐 回顾一下类加载过程 类加载过程 ...

  8. 深入JVM类加载器机制,值得你收藏

    先来一道题,试试水平 public static void main(String[] args) { ClassLoader c1 = ClassloaderStudy.class.getClass ...

  9. JVM:Java 类的加载机制

    虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制. 类的生命周期 类从被加载到虚拟机内 ...

随机推荐

  1. 2.Java程序运行机制

    1.编译型:compile,把所有东西翻译好,若有更新需要重新全部翻译.执行速度够快,对操作系统要求比较低.开发操作系统时用到编译型语言,例如c/c++...运行时需编译整个程序 2.解释性:运行一个 ...

  2. 在eclipse中导入源码

    因为初学java有一个源码项目想要导入,在网上找了很多方法试了都不行,后来发现其实是想多了,这里说一个很简洁的方法.* 1.首先点eclipse中的File然后点import, 2. 然后选Gener ...

  3. 废旧手机利用之装一个Linux系统

    开篇: 在废旧手机变废为宝的路上一直没有停下,做过电脑遥控器,家居监控器,给电脑扩展屏幕以及跟着大佬学过智能机器人,但是都是一时兴趣,除了家具监控器目前正在使用之外其他也没有使用了. 最近在学习Lin ...

  4. Git提交代码和更新代码命令

    微信公众号:非科班的科班关注可了解更多的java教程和其它资源视频.问题或建议,请公众号留言; 1.Git提交代码 利用命令提交代码的步骤:1.1.拉取服务器代码,避免覆盖他人的代码 git pull ...

  5. 用CSS实现横向滚动条

    在进行app制作时,需要使用横向滚动条是内容展示更完善 首先,这是html代码: 这是CSS代码: 要点: 设置显示内容的宽 white-space是防止内容自动折行 overflow-y设置为hid ...

  6. WinSCP整合SecureCRT打开终端

    使用WinSCP直接操作文件非常方便,但是如果用它来连入SSH进行指令交互就不方便了,使用XShell.SecureCRT.Putty等SSH终端软件敲指令却很不错,于是想能不能将两者结合起来使用.我 ...

  7. UML--->活动图梳理

    活动图梳理 活动图 概述 活动图是UML用于对系统的动态行为建模的另一种常用工具,它描述活动的顺序,展现从一个活动到另一个活动的控制流,常常用于描述业务过程和并行处理过程,活动图在本质上是一种流程图. ...

  8. Spring注解开发系列专栏

    这个系列主要是讲Spring注解的使用,可以为后面SpringBoot的学习带来一定的帮助.我觉得从Spring直接过度到SpringBoot还是有点快,还是得需要一个演变的过程.从Spring开发, ...

  9. 令牌桶算法实现API限流

    令牌桶算法( Token Bucket )和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100 ,则间隔是 10 ...

  10. 使用Python通过SMTP发送邮件

    有些业务可能由于各种各样的原因并不适用于Zabbix监控,这时如果要做到系统出问题能立即发送邮件,就需要自己来写监控脚本了,出问题要实时通过邮件报警,以下案例使用Python脚本实现通过SMTP协议发 ...