JVM 原理和实践

《Java 虚拟机原理》7.2 精选 —— 类加载篇

2021-02-22  本文已影响0人  熊本极客

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 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

image.png

2.JVM 为什么采用双亲委派模型

双亲委派模型是 Java 类加载器的工作机制

(1)双亲委派模型的工作原理

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

image.png

双亲委派模型的关键代码

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;
        }
    }
1614343160.png

(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 已经被回收

注意:满足上述条件,“可以”对该对象进行回收,而不是“马上”、“必然”回收。

上一篇 下一篇

猜你喜欢

热点阅读