JVM系列(一) - JVM简介以及类加载器

一. 什么是JVM

Java Virtual Machine, 即Java虚拟机, 简称JVM. JVM是对计算机的模拟, 并不是真正的计算机. 可以这么理解. JVM就是运行在内存中的一个程序.

Java程序号称"一次编译, 到处运行", 而JVM则是Java程序可以到处运行的关键, 因为JVM屏蔽了不同操作系统的底层实现. 不同的操作系统有与之对应的JDK. 比如JDK有Windows, Linux版本, 在不同的操作系统上安装不同的JDK.

二. JDK 与 JRE

image

可以清晰看到, JDK包含JRE以及常用工具. 比如执行命令java, 编译命令javac, 反编译命令javap等.
JRE是Java Runtime Environment的简称, 中文是Java运行环境. 在按装JDK时, 可以只安装JRE即可正常运行编译好的Java程序.而JVM是整个Java的运行基础. 我们常用的JVM是Java HotSpot. 采用解释和编译混合执行的模式.

java语言的跨平台性

针对不同的系统, 有与之对应的JDK. 源文件只有一个, 但是源文件会根据JDK的版本, 会解释成不同的机器语言.


Java的跨平台性.png

三. 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时, 会经过如下过程:


类执行过程.png

抛开JVM实现的部分, 我们主要看类的加载过程, 也就是图中的classLoader.loadClass(), 而loadClass的过程入下

类加载过程.png

当我们书写好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. 双亲委派机制

总结下来: 向上检查, 向下加载


image.png
  • 向上检查
    比如当加载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 双亲委派机制的好处

  1. 沙箱安全机制: 确保jdk核心类库不会被恶意修改
  2. 类加载的唯一性: 当父类已经加载了该类, 子类就没必要再加载一遍

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.

附录

  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

你可能感兴趣的:(JVM系列(一) - JVM简介以及类加载器)