一、JVM类加载过程及源码分析
我们通过ide写的java代码,毫无疑问是最终需要加载到JVM来运行的。试想JVM作为跨语言的平台,能同时支持多种编程语言(js、groory、scala…等)的字节码文件运行,那么在字节码文件和JVM之间,必须有一套完备的流程,来将字节码文件转化为JVM内存中的变量信息。 下图,是坊间盛传的JVM类加载过程,今天我们来探一探水深。
1.1 前言引语
sun.misc.Launcher的方法很简单,静态代码实例了launcher,供getLauncher()方法获取launcher对象。 但是,当给我们他通过debug打点到launcher()构造方法获取ExtClassLoader、AppClassLoader时,却发现断点进不到46、52行。是思路错了还是断点无效?
于是我又翻了翻 IBM 关于 Java 中 Debug 实现原理的介绍,文章地址如下:https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/ JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。 参考 Oracle 的官方文档:https://docs.oracle.com/javase/9/docs/api/jdk.jdi-summary.html 可以知道 jdi 是一个位于 tools.jar 包下的子包,而 tools.jar 也是由 BootStrap 类加载器负责加载的
可以明确,Debug时调用的是lib/tools.jar的包,而此时启动类加载器势必已经完成启动,所以debug无效。
1.2类加载器
类加载器定义:
- 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的类
- 自定义加载器:负责加载用户自定义路径下的类包
JVM中默认读取的路径参数:
class_path = SecuritySupport.getSystemProperty("java.class.path");
boot_path = SecuritySupport.getSystemProperty("sun.boot.class.path");
ext_path = SecuritySupport.getSystemProperty("java.ext.dirs");
上节了解到,启动类加载器比较神秘,多是由C++来实现的,那我们就先从Luncher着手看看扩展类加载器和应用程序类加载器的结构。
1、首先查看Launcher的结构,内部类有AppClassLoader、ExtClassLoader ,并且这2个内部类都继承了URLClassLoader。下图看URLClassLoader的组织结构,最终继承了ClassLoader类。所以我们需要明确,类加载器之间并没有继承的父子关系,而是ClassLoader中维护了private final ClassLoader parent;字段,用这种方式来标记当前加载器的父加载器。后面在看到双亲委委派机制时,不能搞混了。 2、Luncher使用的ClassLoader ,是由getAppClassLoader()生成的,所以JVM的加载的类,首先都需要从App加载器开始加载。 Luncher的构造方法,生成 ExtClassLoader 、AppClassLoader。
private ClassLoader loader;
public ClassLoader getClassLoader() {
return this.loader;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
}
2、AppClassLoader的getAppClassLoader方法,将ExtClassLoader对象 var0传递给超类ClassLoader,并标记ExtClassLoader 扩展类加载器是AppClassLoader应用程序加载器的父加载器。同时ExtClassLoader 在构造时,传入的父加载器参数是null,但实际其父类加载器是启动类加载器,C++实现的所以在java代码中只出现了BootClassPathHolder。
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<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
1.3 类加载过程及双亲委派机制
类加载的过程中,依次向上委托父类加载器进行加载。这就形成了双亲委派机制。结合loadClass(name)方法和下图看类的加载顺序,就不难理解了。 双亲委派机制简单的说,就是app加载器先向上交由父类加载器进行加载,父类中找不到,再由子类加载器自行加载。 为什么要设计成双亲委派机制?
- 安全机制,防止核心API库被随意篡改。
- 避免重复加载:当父类已经加载过该类时,子类加载器不再加载。保证被加载类的唯一性。
核心图一 类加载过程及双亲委派机制
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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(final String name) throws ClassNotFoundException {
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
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;
}
1.4 类初始化过程
从磁盘中读取字节码文件 —> 转为二进制文件 —> 验证 —> 准备 —> 解析 —> 初始化 这其中多数核心代码,仍然是C++实现,Java代码中看不到太多有效代码,无奈略过。
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1) {
String pkgname = name.substring(0, i);
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
} else {
byte[] b = res.getBytes();
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
}
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
1.4 自定义类加载器
- 预期:
在加载类的时候,从自定义的路径中去查找文件。所以在实现自定义加载器时,只需要把原加载器中定义加载文件的路径替换为我们自己的路径即可。 - 思路:
从上面核心图一中,我们看到最终在查到磁盘中类文件的方法实际是: findClass(name),而扩展类加载器和APP类加载器,实际都没有去实现,而是在父类URLClassLoader中实现的 findClass(name)方法,所以在自定义加载器中,只要重写下方path的路径即可。
protected Class<?> findClass(final String name) throws ClassNotFoundException {
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
}
}
}
}, acc);
}
return result;
}
自定义加载器的功能在1.5中实现。
1.5 打破双亲委派机制
- 预期:打破双亲委派的目的,就是在加载某个类时,不去父类中完成类的加载,而是自行实现加载的逻辑。
- 思路:JVM中的加载器在定义加载类时,实际是继承或重写loadClass()来实现的。正如核心图一中的代码逻辑,我们只要不再使用原有的loadClass()逻辑,自行实现自由的加载逻辑即可。
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String path){
this.classPath = path;
}
private byte[] loadByte(String name) throws Exception {
String path = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + path + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException{
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
@Override
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(name.startsWith("com.asky")){
c = findClass(name);
}else {
c = this.getParent().loadClass(name);
}
} catch (ClassNotFoundException e) {
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/study");
Class clazz = classLoader.loadClass("com.asky.test");
Object obj = clazz.newInstance();
System.out.println(clazz.getClassLoader());
}
}
参考文章:https://cloud.tencent.com/developer/article/1590312
|