一文带你深扒ClassLoader内核,揭开它的神秘面纱

Code:

stack=2, locals=2, args_size=1

0: ldc           #7                  // String 关注【我没有三颗心脏】,关注更多精彩

2: astore_1

3: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;

6: aload_1

7: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

10: return

LineNumberTable:

line 4: 0

line 5: 3

line 6: 10

}

SourceFile: “Tester.java”

可以看到,上面定义的 str 变量在编译阶段会被解析称为 符号引用,符号引用的标志是 astore_,这里就是 astore_1。

store_1的含义是将操作数栈顶的 关注【我没有三颗心脏】,关注更多精彩保存回索引为 1 的局部变量表中,此时访问变量 str 就会读取局部变量表索引值为 1 中的数据。所以局部变量 str 就是一个符号引用。

再来看另外一个例子:

public class Tester {

public static void main(String[] args) {

System.out.println(“关注【我没有三颗心脏】,关注更多精彩”);

}

}

这一段代码反编译之后得到如下的代码:

// 上面是类的详细信息省略…

{

// …

public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)V

flags: (0x0009) ACC_PUBLIC, ACC_STATIC

Code:

stack=2, locals=1, args_size=1

0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc           #13                 // String 关注【我没有三颗心脏】,关注更多精彩

5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

LineNumberTable:

line 4: 0

line 5: 8

}

SourceFile: “Tester.java”

我们可以看到这里直接使用了 ldc 指令将 关注【我没有三颗心脏】,关注更多精彩推送到了栈,紧接着就是调用指令 invokevirtual,并没有将字符串存入局部变量表中,这里的字符串就是一个 直接引用

第三步:初始化(Initialization)

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

初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:

  • 1️⃣ 声明类变量是指定初始值;

  • 2️⃣ 使用静态代码块为类变量指定初始值;

JVM 初始化步骤:

  • 1️⃣ 假如这个类还没有被加载和连接,则程序先加载并连接该类

  • 2️⃣ 假如该类的直接父类还没有被初始化,则先初始化其直接父类

  • 3️⃣ 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下几种:

  • 创建类的实例,也就是 new 的方式

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

  • 调用类的静态方法

  • 反射(如 Class.forName(“com.wmyskxz.Tester”))

  • 初始化某个类的子类,则其父类也会被初始化

  • Java 虚拟机启动时被标明为启动类的类,直接使用 java.exe 命令来运行某个主类

  • 使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHanlde 实例最后的解析结果为 REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄时,都需要先初始化该句柄对应的类

  • 接口中定义了 JDK 8 新加入的默认方法(default修饰符),实现类在初始化之前需要先初始化其接口

Part 4. 深入理解双亲委派模型

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

我们在上面已经了解了一个类是如何被加载进 JVM 的——依靠类加载器——在 Java 语言中自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载 核心类库,%JRE_HOME%\lib 下的rt.jar、resources.jar、charsets.jar 和 class 等。

  • Extention ClassLoader 扩展的类加载器,加载目录 %JRE_HOME%\lib\ext 目录下的jar 包和 class 文件。

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

我们可以通过一个简单的例子来简单了解 Java 中这些自带的类加载器:

public class PrintClassLoader {

public static void main(String[] args) {

printClassLoaders();

}

public static void printClassLoaders() {

System.out.println(“Classloader of this class:”

+ PrintClassLoader.class.getClassLoader());

System.out.println(“Classloader of Logging:”

+ com.sun.javafx.util.Logging.class.getClassLoader());

System.out.println(“Classloader of ArrayList:”

+ java.util.ArrayList.class.getClassLoader());

}

}

上方程序打印输出如下:

Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2

Classloader of Logging:sun.misc.Launcher$ExtClassLoader@60e53b93

Classloader of ArrayList:null

如我们所见,这里分别对应三种不同类型的类加载器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(显示为 null)。

一个很好的问题是:Java 类是由 java.lang.ClassLoader 实例加载的,但类加载器本身也是类,那么谁来加载类加载器呢?

我们假装不知道,先来跟着源码一步一步来看。

先来看看 Java 虚拟机入口代码

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

在 JDK 源码 sun.misc.Launcher 中,蕴含了 Java 虚拟机的入口方法:

public class Launcher {

private static Launcher launcher = new Launcher();

private static String bootClassPath =

System.getProperty(“sun.boot.class.path”);

public static Launcher getLauncher() {

return launcher;

}

private ClassLoader loader;

public Launcher() {

// Create the extension class loader

ClassLoader extcl;

try {

extcl = ExtClassLoader.getExtClassLoader();

} catch (IOException e) {

throw new InternalError(

“Could not create extension class loader”, e);

}

// Now create the class loader to use to launch the application

try {

loader = AppClassLoader.getAppClassLoader(extcl);

} catch (IOException e) {

throw new InternalError(

“Could not create application class loader”, e);

}

// 设置 AppClassLoader 为线程上下文类加载器,这个文章后面部分讲解

Thread.currentThread().setContextClassLoader(loader);

}

/*

* Returns the class loader used to launch the main application.

*/

public ClassLoader getClassLoader() {

return loader;

}

/*

* The class loader used for loading installed extensions.

*/

static class ExtClassLoader extends URLClassLoader {}

/**

* The class loader used for loading from java.class.path.

* runs in a restricted security context.

*/

static class AppClassLoader extends URLClassLoader {}

}

源码有精简,但是我们可以得到以下信息:

1️⃣ Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

2️⃣ Launcher 没有看到 Bootstrap ClassLoader 的影子,但是有一个叫做 bootClassPath的变量,大胆一猜就是 Bootstrap ClassLoader 加载的 jar 包的路径。

(ps: 可以自己尝试输出一下 System.getProperty(“sun.boot.class.path”) 的内容,它正好对应了 JDK 目录 lib 和 classes 目录下的 jar 包——也就是通常你配置环境变量时设置的%JAVA_HOME/lib 的目录了——同样的方式你也可以看看 Ext 和 App 的源码)

3️⃣ ExtClassLoader 和 AppClassLoader 都继承自 URLClassLoader,进一步查看 ClassLoader 的继承树,传说中的双亲委派模型也并没有出现。(甚至看不到 Bootstrap ClassLoader 的影子,Ext 也没有直接继承自 App 类加载器)

一文带你深扒ClassLoader内核,揭开它的神秘面纱

ClassLoader 继承树

(⚠️注意,这里可以明确看到每一个 ClassLoader 都有一个 parent 变量,用于标识自己的父类,下面详细说)

4️⃣ 注意以下代码:

ClassLoader extcl;

extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

分别跟踪查看到这两个 ClassLoader 初始化时的代码:

// 一直追踪到最顶层的 ClassLoader 定义,构造器的第二个参数标识了类加载器的父类

private ClassLoader(Void unused, ClassLoader parent) {

this.parent = parent;

// 代码省略…

}

// Ext 设置自己的父类为 null

public ExtClassLoader(File[] var1) throws IOException {

super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);

SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);

}

// 手动把 Ext 设置为 App 的 parent(这里的 var2 是传进来的 extc1)

AppClassLoader(URL[] var1, ClassLoader var2) {

super(var1, var2, Launcher.factory);

this.ucp.initLookupCache(this);

}

由此,我们得到了这样一个类加载器的关系图:

一文带你深扒ClassLoader内核,揭开它的神秘面纱

类加载器的父类都来自哪里?

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

奇怪,为什么 ExtClassLoader 的 parent 明明是 null,我们却一般地认为 Bootstrap ClassLoader 才是 ExtClassLoader 的父加载器呢?

答案的一部分就藏在 java.lang.ClassLoader.loadClass() 方法里面:(这也就是著名的「双亲委派模型」现场了)

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 {

// 父加载器为空则调用 Bootstrap ClassLoader

c = findBootstrapClassOrNull(name);

}

} 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();

// 父加载器没有找到,则调用 findclass

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()

resolveClass©;

}

return c;

}

}

代码逻辑很好地解释了双亲委派的原理。

1️⃣ 当前 ClassLoader 首先从 自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。(每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。)

2️⃣ 当前 ClassLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 Bootstrap ClassLoader。(当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。)

一文带你深扒ClassLoader内核,揭开它的神秘面纱

所以,答案的另一部分是因为最高一层的类加载器 Bootstrap 是通过 C/C++ 实现的,并不存在于 JVM 体系内 (不是一个 Java 类,没办法直接表示为 ExtClassLoader 的父加载器),所以输出为 null。

(我们可以很轻易跟踪到 findBootstrapClass() 方法被 native 修饰:private native Class<?> findBootstrapClass(String name);)

➡️ OK,我们理解了为什么 ExtClassLoader 的父加载器为什么是表示为 null 的 Bootstrap 加载器,那我们 自己实现的 ClassLoader 父加载器应该是谁呢?

观察一下 ClassLoader 的源码就知道了:

protected ClassLoader(ClassLoader parent) {

this(checkCreateClassLoader(), parent);

}

protected ClassLoader() {

this(checkCreateClassLoader(), getSystemClassLoader());

}

类加载器的 parent 的赋值是在 ClassLoader 对象的构造方法中,它有两个情况:

1️⃣ 由外部类创建 ClassLoader是直接指定一个 ClassLoader 为 parent;

2️⃣ 由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 通过 getClassLoader() 获取,也就是 AppClassLoader。直白的说,一个 ClassLoader 创建时如果没有指定 parent,那么它的 parent 默认就是 AppClassLoader。(建议去看一下源码)

为什么这样设计呢?

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

简单来说,主要是为了 安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String,同时也 避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛 java.lang.ClassCaseException。

如果我们要实现自己的类加载器,不管你是直接实现抽象类 ClassLoader,还是继承 URLClassLoader 类,或者其他子类,它的父加载器都是 AppClassLoader。

因为不管调用哪个父类构造器,创建的对象都必须最终调用 getSystemClassLoader() 作为父加载器 (我们已经从上面的源码中看到了)。而该方法最终获取到的正是 AppClassLoader_(别称 SystemClassLoader)_。

这也就是我们熟知的最终的双亲委派模型了。

一文带你深扒ClassLoader内核,揭开它的神秘面纱

Part 5. 实现自己的类加载器

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

什么情况下需要自定义类加载器

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

在学习了类加载器的实现机制之后,我们知道了双亲委派模型并非强制模型,用户可以自定义类加载器,在什么情况下需要自定义类加载器呢?

1️⃣ 隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载器到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包。

2️⃣ 修改类加载方式。类的加载模型并非强制,除了 Bootstrap 外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需的动态加载。

3️⃣ 扩展加载源。比如从数据库、网络,甚至是电视机顶盒进行加载。(下面我们会编写一个从网络加载类的例子)

4️⃣ 防止源码泄露。Java 代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。

一个常规的例子

===========

实现一个自定义的类加载器比较简单:继承ClassLoader,重写 findClass() 方法,调用 defineClass() 方法,就差不多行了。

Tester.java

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

我们先来编写一个测试用的类文件:

public class Tester {

public void say() {

System.out.println(“关注【我没有三颗心脏】,解锁更多精彩!”);

}

}

在同级目录下执行 javac Tester.java 命令,并把编译后的 Tester.class 放到指定的目录下(我这边为了方便就放在桌面上啦 /Users/wmyskxz/Desktop)

MyClassLoader.java

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

我们编写自定义 ClassLoader 代码:

import java.io.ByteArrayOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

public class MyClassLoader extends ClassLoader {

private final String mLibPath;

public MyClassLoader(String path) {

// TODO Auto-generated constructor stub

mLibPath = path;

}

@Override

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

// TODO Auto-generated method stub

String fileName = getFileName(name);

File file = new File(mLibPath, fileName);

try {

FileInputStream is = new FileInputStream(file);

ByteArrayOutputStream bos = new ByteArrayOutputStream();

int len = 0;

try {

while ((len = is.read()) != -1) {

bos.write(len);

}

} catch (IOException e) {

e.printStackTrace();

}

byte[] data = bos.toByteArray();

is.close();

bos.close();

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

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

return super.findClass(name);

}

// 获取要加载的 class 文件名

private String getFileName(String name) {

// TODO Auto-generated method stub

int index = name.lastIndexOf(‘.’);

if (index == -1) {

return name + “.class”;

} else {

return name.substring(index + 1) + “.class”;

}

}

}

我们在 findClass() 方法中定义了查找 class 的方法,然后数据通过 defineClass() 生成了 Class 对象。

ClassLoaderTester 测试类

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

我们需要删除刚才在项目目录创建的 Tester.java 和编译后的 Tester.class 文件来观察效果:

import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;

public class ClassLoaderTester {

public static void main(String[] args) {

// 创建自定义的 ClassLoader 对象

MyClassLoader myClassLoader = new MyClassLoader(“/Users/wmyskxz/Desktop”);

try {

// 加载class文件

Class<?> c = myClassLoader.loadClass(“Tester”);

if(c != null){

try {

Object obj = c.newInstance();

Method method = c.getDeclaredMethod(“say”,null);

//通过反射调用Test类的say方法

method.invoke(obj, null);

} catch (InstantiationException | IllegalAccessException

| NoSuchMethodException

| SecurityException |

IllegalArgumentException |

InvocationTargetException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

} catch (ClassNotFoundException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

写在最后

作为一名即将求职的程序员,面对一个可能跟近些年非常不同的 2019 年,你的就业机会和风口会出现在哪里?在这种新环境下,工作应该选择大厂还是小公司?已有几年工作经验的老兵,又应该如何保持和提升自身竞争力,转被动为主动?

就目前大环境来看,跳槽成功的难度比往年高很多。一个明显的感受:今年的面试,无论一面还是二面,都很考验Java程序员的技术功底。

最近我整理了一份复习用的面试题及面试高频的考点题及技术点梳理成一份“Java经典面试问题(含答案解析).pdf和一份网上搜集的“Java程序员面试笔试真题库.pdf”(实际上比预期多花了不少精力),包含分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货!

由于篇幅有限,为了方便大家观看,这里以图片的形式给大家展示部分的目录和答案截图!

Java经典面试问题(含答案解析)

阿里巴巴技术笔试心得

ion

| NoSuchMethodException

| SecurityException |

IllegalArgumentException |

InvocationTargetException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

} catch (ClassNotFoundException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

写在最后

作为一名即将求职的程序员,面对一个可能跟近些年非常不同的 2019 年,你的就业机会和风口会出现在哪里?在这种新环境下,工作应该选择大厂还是小公司?已有几年工作经验的老兵,又应该如何保持和提升自身竞争力,转被动为主动?

就目前大环境来看,跳槽成功的难度比往年高很多。一个明显的感受:今年的面试,无论一面还是二面,都很考验Java程序员的技术功底。

最近我整理了一份复习用的面试题及面试高频的考点题及技术点梳理成一份“Java经典面试问题(含答案解析).pdf和一份网上搜集的“Java程序员面试笔试真题库.pdf”(实际上比预期多花了不少精力),包含分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货!

由于篇幅有限,为了方便大家观看,这里以图片的形式给大家展示部分的目录和答案截图!
[外链图片转存中…(img-ZvsLSUFD-1714543623029)]

Java经典面试问题(含答案解析)

[外链图片转存中…(img-fw0632mr-1714543623030)]

阿里巴巴技术笔试心得

[外链图片转存中…(img-6Qa5z6Uq-1714543623030)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 28
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值