《Java 虚拟机原理》7.2 精选 —— 类加载篇
1.描述一下 JVM 加载 class 文件的机制
说明:
Java 中的所有类都需要被类加载器加载到 JVM 中(类加载本身也是一个类,其主要工作是把 class 文件从磁盘读取到内存中)。
1.1 类加载的方式有两种:
① 隐式加载,程序运行过程中通过 new 指令实例化对象,即把类隐式地加载到 JVM 中;
例子1:加载一个类的时候,类加载会隐式加载它的父类,例如,Child 继承 Parent,Class.forName("Child") 的时候,会加载 Parent
例子2:执行一个类之前,类加载会隐式加载它全部的依赖类(全盘责任机制),例如,执行 WordCount 的 main 方法之前,JVM 会自动加载 FlatMapFunction、...、WordCountData 等依赖类,也会自动加载 FlatMapFunction 的 Public、Collector、Function 等依赖类
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.examples.wordcount.util.WordCountData;
public class WordCount {
public static void main(String[] args) throws Exception {
// 忽略
}
}
import org.apache.flink.annotation.Public;
import org.apache.flink.util.Collector;
import org.apache.flink.api.common.functions.Function;
public interface FlatMapFunction<T, O> extends Function, Serializable {
// 忽略
}
全盘责任机制:
当一个 ClassLoader 装载一个类时,除非显式另一个 ClassLoader,该类所依赖的类也由这个 ClassLoader 负责加载。
② 显示加载,通过反射 Class.forName() 等方法,把类显式地加载到 JVM 中;
this.getclass().getClassLoader().loadClass();
Class.forName("WordCount");
1.2 类加载器的模型及其类型
类加载器采用双亲委派模型, 其类型有 4 种:
① Bootstrap ClassLoader 启动类加载器:负责加载系统类和 /lib 目录的 jar 和类,例如 String
② ExtClassLoader 扩展类加载器:负责加载 /lib/ext 目录下的 jar 和类
③ AppClassLoader 应用程序类加载器:负责加载当前应用 ClassPath 的 jar 和类
④ UserDefinedClassLoader 用户自定义加载器:负责加载用户自定义的 jar 和类
2.3 类加载的过程
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

2.JVM 为什么采用双亲委派模型
双亲委派模型是 Java 类加载器的工作机制。
(1)双亲委派模型的工作原理
双亲委派模型是指如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
注意:双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器。

双亲委派模型的关键代码
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) {
c = parent.loadClass(name, false);
} else {
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);
// 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;
}
}

(2)采用双亲委派机制的优点
① 避免重复加载类。注意:相同的类文件,被不同类加载器加载产出的是两个不同的类。
② 避免 Java 核心 API 被篡改,保证程序稳定运行。用户自定义编写 java.lang.Object 类,如果没有双亲委派模型,每个类加载器加载各自的类,导致系统出现多个不同的 Object 类。
(3)如何实现热加载类
参考 Flink 的 child-first 类加载机制
public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {
// 省略构造函数等
@Override
protected synchronized Class<?> loadClassWithoutExceptionHandling(
String name,
boolean resolve) throws ClassNotFoundException {
// 首先, 检查该类是否被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
// 省略...
try {
// 根据 URL 选择类
c = findClass(name);
} catch (ClassNotFoundException e) {
// let URLClassLoader do it, which will eventually call the parent
c = super.loadClassWithoutExceptionHandling(name, resolve);
}
}
// 省略...
}
// 重写获取资源路径, 避免使用父加载器的 getResource 方法
@Override
public URL getResource(String name) {
// 使用 URLClassloader 的 getResource
URL urlClassLoaderResource = findResource(name);
if (urlClassLoaderResource != null) {
return urlClassLoaderResource;
}
// delegate to super
return super.getResource(name);
}
// 重写获取资源路径, 避免使用父加载器的 getResources 方法
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// f使用 URLClassloader 的 getResources
Enumeration<URL> urlClassLoaderResources = findResources(name);
final List<URL> result = new ArrayList<>();
while (urlClassLoaderResources.hasMoreElements()) {
result.add(urlClassLoaderResources.nextElement());
}
// 省略...
}
}
ChildFirstClassLoader 的核心代码是 loadClassWithoutExceptionHandling 方法,没有采用父加载器 findClass(避免采用双亲委派模型),而是采用自定义加载器 URLClassLoader 的findClass,同时重写 getResource 方法,即使用 URLClassLoader 获取资源的 URL。
3.如何判断一个类使无用的类
“无用的类”需要满足一下条件:
① 该类所有的实例都已经被回收,即 JVM Heap 里不存在该类的任何实例;
② 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
③ 加载该类的 ClassLoader 已经被回收。
注意:满足上述条件,“可以”对该对象进行回收,而不是“马上”、“必然”回收。