一. 什么是JVM
Java Virtual Machine, 即Java虚拟机, 简称JVM. JVM是对计算机的模拟, 并不是真正的计算机. 可以这么理解. JVM就是运行在内存中的一个程序.
Java程序号称"一次编译, 到处运行", 而JVM则是Java程序可以到处运行的关键, 因为JVM屏蔽了不同操作系统的底层实现. 不同的操作系统有与之对应的JDK. 比如JDK有Windows, Linux版本, 在不同的操作系统上安装不同的JDK.
二. JDK 与 JRE
可以清晰看到, JDK包含JRE以及常用工具. 比如执行命令java, 编译命令javac, 反编译命令javap等.
JRE是Java Runtime Environment的简称, 中文是Java运行环境. 在按装JDK时, 可以只安装JRE即可正常运行编译好的Java程序.而JVM是整个Java的运行基础. 我们常用的JVM是Java HotSpot. 采用解释和编译混合执行的模式.
java语言的跨平台性
针对不同的系统, 有与之对应的JDK. 源文件只有一个, 但是源文件会根据JDK的版本, 会解释成不同的机器语言.
三. JVM的组成
主要由三部分组成: 类文件加载子系统, JVM运行时数据区, 字节码执行引擎
今天主要介绍的是类文件的加载 - 类加载器
首先我们思考一个问题: 一个类文件是如何被加载到JVM中的?
1. 类的加载过程
假如当前我们有这么一个类: com.zte.Test.java
package com.zte;
public class Test{
public static void main(String[] args) {
System.out.println("Hello World");
}
}
通常, 当我们运行Test.java时, 会经过如下过程:
抛开JVM实现的部分, 我们主要看类的加载过程, 也就是图中的classLoader.loadClass(), 而loadClass的过程入下
当我们书写好java源文件, 通过javac命令可以将源文件编译成class文件(具体操作参考附录1).
而class文件加载到JVM一共分为三步: 加载(load) , 链接(link) , 初始化(init), 其中链接(link)又分为验证, 准备和解析三个阶段.
- 加载(load) : load阶段读取class文件产生二进制流, 转化为特定的数据结构, 并验证魔数, 常量池, 文件长度, 是否有父类等信息, 然后创建类的Class实例.
- 链接(link) : link阶段包括验证, 准备, 解析三个阶段. 验证主要是验证类是否符合语法;准备阶段是给静态变量分配内存空间, 并设置默认值; 解析是将符号引用替换为直接引用.这一阶段会将静态方法(比如main()方法)替换为方法在内存中的地址.这就是静态连接, 而动态链接是在程序运行期间将符号引用替换为直接引用.
- 初始化(init) : init阶段是为静态变量赋初始值, 执行静态代码.因此, 静态代码是在类第一次加载的时候执行, 并且只会执行一次.此时还没有该类的实例.
package com.learn.jvm;
public class TestDynamicLoad {
static {
System.out.println("********** load TestDynamicLoad ************");
}
public static void main(String[] args) {
new A();
System.out.println("************ load test ***************");
B b = null; // 不会被加载, 除非使用new B()
}
}
class A {
static {
System.out.println("***************** load A *****************");
}
public A() {
System.out.println("****************** init A *****************");
}
}
// 输出
********** load TestDynamicLoad ************
***************** load A *****************
****************** init A *****************
************ load test ***************
类被使用到的时候才会被加载.
2. 类加载器
Java通过类加载器把类的实现和类的定义进行解耦.
Java中类加载器通常分为引导类加载器(Bootstrap ClassLoader), 扩展类加载器(Ext ClassLoader), 应用类加载器(AppClassLoader)以及自定义类加载.
- 引导类加载器
在JVM启动时加载, 负责加载Java的核心类库, 比如Object, String等.主要是jre\lib下的rt.jar. 它是通过C++实现的, 不存在于Java体系中, 因此获取不到 - 扩展类加载器
引导类加载器加载JVM启动器com.mics.Launcher. 通过Launcher创建扩展类加载器以及应用类加载器.扩展类加载器负责加载jre\lib\ext目录下的文件, 加载一些扩展的系统类, 比如xml, 加密, 压缩相关的功能. 在jdk1.9之前是扩展类加载器, jdk1.9以后是平台类加载器(Platform ClassLoader) - 应用类加载器
负责加载用户定义的CLASSPATH路径下的类. - 自定义加载器
用户可以自定义类加载器
3. 双亲委派机制
总结下来: 向上检查, 向下加载
- 向上检查
比如当加载Test.java时,先从AppClassLoader开始检查,通过。findLoadedClass方法检查当前类加载器是否加载过该类,如果没有,则通过ExtClassLoader继续检查,如果还没有,再通过Bootstrap ClassLoader检查是否加载过。 - 向下加载
而向下加载的意思是从BootstrapClassLoader开始尝试加载,通过findClass()方法尝试加载类, 加载不了会使用子类加载器尝试加载,直到AppClassLoader。如果还是加载不了,那就只能报错了, 抛出ClassNotFoundException
3.1 类加载器源码阅读
引导类加载器我们就不看了, 前文说过它是由C++实现的, 想看的话就只能看JVM的源码了. 我们从引导类加载的JVM启动器Launcher开始看(本文的jdk是1.8)
- Launcher.java
package sun.misc;
import ...
public class Launcher {
// 可以理解Launcher是单例的
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
return launcher;
}
// 构造方法
public Launcher() {...}
// 静态内部类, 应用类加载器和扩展类加载器
static class AppClassLoader extends URLClassLoader {...}
static class ExtClassLoader extends URLClassLoader {...}
}
上面是我认为Launcher类里比较重要的点.
首先来看Launcher的构造方法
package sun.misc;
import ...
public class Launcher {
...
public Launcher() {
// 扩展类加载器的引用
Launcher.ExtClassLoader var1;
try {
// 调用内部静态类ExtClassLoader.getExtClassLoader()方法
// 该方法主要是用来创建扩展类加载器, 并设置其父类加载器为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 创建应用类加载器
// 注意: 此处有2个点比较重要
// 1. 创建应用类加载器时, 传入扩展类加载器的引用var1
// 2. 将应用类加载器赋值给this.loader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 设置
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
...
}
构造器中重要的点都已标注出来了, 接下来就是看扩展类加载器以及应用类加载器是如何创建的.
首先看扩展类加载器的创建过程
- ExtClassLoader
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
// DCL: Double-Checked Locking 双重检查锁, 实现延迟初始化实例
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
// 真正的创建逻辑
instance = createExtClassLoader();
}
}
}
return instance;
}
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Launcher.ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
// 调用ExtClassLoader的构造函数
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
public ExtClassLoader(File[] var1) throws IOException {
// 此处有1个关键点
// 第二个参数是null
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this)
.initLookupCache(this);
}
}
至此, 我们跟到了ExtClassLoader的构造方法, 在其构造方方法中, 调用了父类构造方法super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); 继续跟代码, 最终是调用了ClassLoader的构造方方法
package java.lang;
import ...
public abstract class ClassLoader {
// 保存父类加载器的引用
private final ClassLoader parent;
// 此时是创建ExtClassLoader, 前文说到第二个参数是null, 即parent = null
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
}
可以看到, ClassLoader有一个成员变量parent, 在创建ExtClassLoader时, 传入的parent为null, 即为BootstrapClassLoader. 后文会讲, 为什么是BootstrapClassLoader
接下来, 继续看AppClassLoader的创建
- AppClassLoader.
// Launcher的构造方法, 此处就不在赘述了, 直接看AppClassLoader的getAppClassLoader方法
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
// 同样的, 通过构造方法创建AppClassLoader
// 但是, 此时的var0就是ExtClassLoader对象实例
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
}
AppClassLoader的构造方法同样是调用了父类的构造方法, 但是, 此时第二个参数是ExtClassLoader, 最终进入ClassLoader的构造方法时, parent = ExtClassLoader.
至此, 通过组合的方式, 构建了类加载器的父子关系. 也就是我们上文介绍的那样, AppClassLoader的父类加载器是ExtClassLoader, ExtClassLoader的父加载器是null, 也就是BootstrapClassLoader.
3.2 自定义类加载器
了解了类加载器, 那么如何自定义类加载器呢.
在自定义类加载器前, 我们首先要了解, 类是如何向上检查以及向下加载的.
当我们执行main方法时, JVM会先调用LauncherHelper.java这个类的checkAndLoadMain()方法来加载main方法所在的类.
package sun.launcher;
import ...
public enum LauncherHelper {
private static final ClassLoader scloader = ClassLoader.getSystemClassLoader();
public static Class> checkAndLoadMain(boolean var0, int var1, String var2) {
// 省略无关紧要的代码
...
// String var3 = getMainClassFromJar(var2); 获取main函数所在的类路径
Class var4 = null;
try {
// 调用常量的loadClass方法
var4 = scloader.loadClass(var3);
} catch (ClassNotFoundException | NoClassDefFoundError var8) {
if (System.getProperty("os.name", "").contains("OS X") && Normalizer.isNormalized(var3, Form.NFD)) {
try {
var4 = scloader.loadClass(Normalizer.normalize(var3, Form.NFC));
} catch (ClassNotFoundException | NoClassDefFoundError var7) {
abort(var8, "java.launcher.cls.error1", var3);
}
} else {
abort(var8, "java.launcher.cls.error1", var3);
}
}
...
// 省略无关紧要的代码
}
}
LauncherHelper.java有个常量对象scloader, 最终也是调用了scloader对象的loadClass方法, 那么, 我们来看看scloader对象是个什么东西.
- ClassLoader.getSystemClassLoader();
private static ClassLoader scl;
public static ClassLoader getSystemClassLoader() {
// 关键方法, 设置scl为this.loader指向的类加载器
// 前文已经介绍了AppClassLoader对象实例赋值给了this.loader
initSystemClassLoader();
if (scl == null) {
return null;
}
...
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
...
}
...
}
package sun.misc;
import ...
public class Launcher {
private ClassLoader loader;
}
public ClassLoader getClassLoader() {
return this.loader;
}
前文已经介绍了this.loader指向的是AppClassLoader对象, 也就是说最终调用的是AppClassLoader的loadClass方法.
那么, 接下来就是看AppClassLoader的loaderClass方法, 终于到了双亲委派机制的核心源码了.
- AppClassLoader.loaderClass()
static class AppClassLoader extends URLClassLoader {
public Class> loadClass(String var1, boolean var2) throws ClassNotFoundException {
...
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
// 最后调用的是父类的loadClass方法
return super.loadClass(var1, var2);
}
}
}
- super.loadClass(var1, var2)
package java.lang;
import ...
public abstract class ClassLoader {
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 加锁, 防止并发, 每个类只能加载一次
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 从源码的英文注释也可以看出, 首先要检查是否已经加载过
Class> c = findLoadedClass(name);
// 还没加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 看看是否还有父类加载器
if (parent != null) {
// 调用父类加载器的loadClass方法
c = parent.loadClass(name, false);
} else {
// 父类加载器为null时, 通过BootstrapClassLoader检查
// 也是通过此处代码说明ExtClassLoader的父类加载器是引导类加载器的
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();
// 加载类
c = findClass(name);
...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
loadClass方法中, 有三个重要的方法, 一是findLoadedClass方法, 二是findClass方法, 三是parent.loadClass()方法
findLoadedClass方法是检查类是否加载过, findClass是尝试加载类, parent.loadClass是调用父类加载器的loadClass方法.那么此时的parent是ExtClassLoader, ExtClassLoader本身没有loadClass方法, 所以最终还是调用了ExtClassLoader的父类ClassLoader中的loadClass方法, 也就是上面的源代码.
- findLoadedClass
那么,就继续看findLoadedClass方法
protected final Class> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class> findLoadedClass0(String name);
最终是调用了本地方法, 没什么好看的, 继续看findClass方法
- findClass
package java.lang;
import ...
public abstract class ClassLoader {
// 钩子方法, 本身并没有做任何实现, 而是留给子类实现
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
package java.net;
import ...
public class URLClassLoader extends SecureClassLoader implements Closeable {
protected Class> findClass(final String name) throws ClassNotFoundException {
final Class> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction>() {
public Class> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 加载class文件
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
}
ClassLoader中并没有对findClass做出实现, 而是留了一个钩子方法给子类实现. 最终是在URLClassLoader中实现了findClass方法, 在加载器的类路径里查找并加载该类
总结: 双亲委派机制核心的两个方法是loadClass和findClass, 自定义类加载器的话, 只要继承ClassLoader并重写findClass方法即可。
package com.learn.jvm.classloader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
String path = this.path + name.replace('.', '/').concat(".class");
byte[] classBytes;
try {
classBytes = Files.readAllBytes(Paths.get(path));
}
catch (IOException e) {
throw new ClassNotFoundException(String.format("class not found in %s", path));
}
return defineClass(name, classBytes, 0, classBytes.length);
}
}
3.4 双亲委派机制的好处
- 沙箱安全机制: 确保jdk核心类库不会被恶意修改
- 类加载的唯一性: 当父类已经加载了该类, 子类就没必要再加载一遍
3.5 全盘委托机制
当一个类被某个ClassLoader加载时, 这个类的所有依赖以及引入的类, 都由这个ClassLoader加载, 除非明确指定其他的ClassLoader
3.6 如何打破双亲委派机制
只需要重写loadClass()方法即可.
package com.learn.jvm.classloader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
String path = this.path + name.replace('.', '/').concat(".class");
byte[] classBytes;
try {
classBytes = Files.readAllBytes(Paths.get(path));
}
catch (IOException e) {
throw new ClassNotFoundException(String.format("class not found in %s", path));
}
return defineClass(name, classBytes, 0, classBytes.length);
}
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
if (name.contains("com.learn.jvm")) {
return this.findClass(name);
}
return super.loadClass(name);
}
}
高频面试题
1.
附录
- java源文件编译成class文件
打开cmd, 进入源文件所在目录, 输入命令javac Test.java, 然后输入dir, 即可看到编译后的class文件
C:\Users\86156>d:
D:\>cd D:\code\svn_code\develop_svn\learn\java-learn\src\main\java\com\learn
D:\code\svn_code\develop_svn\learn\java-learn\src\main\java\com\learn>javac Test.java
D:\code\svn_code\develop_svn\learn\java-learn\src\main\java\com\learn>dir
D:\code\svn_code\develop_svn\learn\java-learn\src\main\java\com\learn 的目录
2021/07/06 23:17 424 Test.class
2021/07/06 23:03 236 Test.java