探索JVM底层之类加载

什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

image

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件

本文目标:

类的生命周期

类加载器

类加载过程

类加载方式

类加载时机

线程上下文类加载器

沙箱安全

1. 类的生命周期

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

image
  • 加载:查找并加载类的二进制数据
  1. 通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取)
  2. 解析成运行时数据,即instanceKlass实例,存放在方法区
  3. 在堆区生成该类的Class对象,即instanceMirrorKlass实例

程序随便你怎么写,随便你用什么语言,只要能达到这个效果即可

就是说你可以改写openjdk源码,你写的程序能达到这三个效果即可

何时加载

主动使用时

  1. new、getstatic、putstatic、invokestatic
  2. 反射
  3. 初始化一个类的子类会去加载其父类
  4. 启动类(main函数所在类)
  5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化

预加载:包装类、String、Thread

因为没有指明必须从哪获取class文件,脑洞大开的工程师们开发了这些

  1. 从压缩包中读取,如jar、war
  2. 从网络中获取,如Web Applet
  3. 动态生成,如动态代理、CGLIB
  4. 由其他文件生成,如JSP
  5. 从数据库读取
  6. 从加密文件中读取

将编译后的.Class静态文件转换到内存中(方法区),然后暴露出来让程序员能访问到。

  • 验证
  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 准备

为静态变量分配内存、赋初值

实例变量是在创建对象的时候完成赋值的,没有赋初值一说

image

如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步

  • 解析

将常量池中的符号引用转为直接引用

解析后的信息存储在ConstantPoolCache类实例中

  1. 类或接口的解析
  2. 字段解析
  3. 方法解析
  4. 接口方法解析

何时解析

思路:

1、加载阶段解析常量池时

2、用的时候

openjdk是第二种思路,在执行特定的字节码指令之前进行解析:

anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield

  • 初始化

执行静态代码块,完成静态变量的赋值

静态字段、静态代码段,字节码层面会生成clinit方法

方法中语句的先后顺序与代码的编写顺序相关

2. 类加载器

JVM中有两种类型的类加载器,由C++编写的及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。

JVM还支持自定义类加载器。后面有写。

各种类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们直接没有从属关系。

image

a. 启动类加载器:

C++实现的,是虚拟机自身的一部分,负责将存放在lib目录中的类库加载到虚拟机内存中。

因为启动类加载器是由C++编写的,通过Java程序去查看显示的是null

因此,启动类加载器无法被Java程序调用

启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的一套逻辑定义为启动类加载器

//查看启动类加载器的加载路径
URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); 
for (URL urL : urLs) {  
  System.out.println(urL); 
}

也可以通过-Xbootclasspath指定

//从openjdk源码
int JNICALL
JavaMain(void * _args)
{
    ……
    mainClass = LoadMainClass(env, mode, what);
    ……
}

static jclass
LoadMainClass(JNIEnv *env, int mode, char *name)
{
    jmethodID mid;
    jstring str;
    jobject result;
    jlong start, end;
    jclass cls = GetLauncherHelperClass(env);
    NULL_CHECK0(cls);
    if (JLI_IsTraceLauncher()) {
        start = CounterGet();
    }
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));

    str = NewPlatformString(env, name);
    CHECK_JNI_RETURN_0(
        result = (*env)->CallStaticObjectMethod(
            env, cls, mid, USE_STDERR, mode, str));

    if (JLI_IsTraceLauncher()) {
        end   = CounterGet();
        printf("%ld micro seconds to load main class\n",
               (long)(jint)Counter2Micros(end-start));
        printf("----%s----\n", JLDEBUG_ENV_ENTRY);
    }

    return (jclass)result;
}

jclass
GetLauncherHelperClass(JNIEnv *env)
{
    if (helperClass == NULL) {
        NULL_CHECK0(helperClass = FindBootStrapClass(env,
                "sun/launcher/LauncherHelper"));
    }
    return helperClass;
}

jclass
FindBootStrapClass(JNIEnv *env, const char* classname)
{
   if (findBootClass == NULL) {
       findBootClass = (FindClassFromBootLoader_t *)dlsym(RTLD_DEFAULT,
          "JVM_FindClassFromBootLoader");
       if (findBootClass == NULL) {
           JLI_ReportErrorMessage(DLL_ERROR4,
               "JVM_FindClassFromBootLoader");
           return NULL;
       }
   }
   return findBootClass(env, classname);
}

JVM_ENTRY(jclass, JVM_FindClassFromBootLoader(JNIEnv* env,
                                              const char* name))
  JVMWrapper2("JVM_FindClassFromBootLoader %s", name);

  // Java libraries should ensure that name is never null...
  if (name == NULL || (int)strlen(name) > Symbol::max_length()) {
    // It's impossible to create this class;  the name cannot fit
    // into the constant pool.
    return NULL;
  }

  TempNewSymbol h_name = SymbolTable::new_symbol(name, CHECK_NULL);
  Klass* k = SystemDictionary::resolve_or_null(h_name, CHECK_NULL);
  if (k == NULL) {
    return NULL;
  }

  if (TraceClassResolution) {
    trace_class_resolution(k);
  }
  return (jclass) JNIHandles::make_local(env, k->java_mirror());
JVM_END

这套逻辑做的事情就是通过启动类加载器加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain,加载main函数所在的类,启动扩展类加载器、应用类加载器也是在这个时候完成的

b. 扩展类加载器

负责将/lib/ext或者被java.ext.dir系统变量所指定路径中的所有类库加载到内存中。

//查看类加载器加载的路径
public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();

        URLClassLoader urlClassLoader = (URLClassLoader) classLoader;

        URL[] urls = urlClassLoader.getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
    }

可以通过java.ext.dirs指定

c. 应用程序类加载器

负责加载用户(ClassPath)上指定的类库,默认加载用户程序的类加载器

//查看类加载器加载的路径
public static void main(String[] args) {
        String[] urls = System.getProperty("java.class.path").split(":");

        for (String url : urls) {
            System.out.println(url);
        }

        System.out.println("================================");

        URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();

        URL[] urls1 = classLoader.getURLs();
        for (URL url : urls1) {
            System.out.println(url);
        }
    }

d. 自定义类加载器:

用户根据需求自己定义,也需要继承自ClassLoader。

类加载器创建链

上面已经讲了启动类加载器没有实体,只是将一段加载逻辑命名成启动类加载器。启动类加载器做的事情是:加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain……启动类、扩展类、应用类加载器逻辑上的父子关系就是在这个方法的调用链中生成的?

1、\openjdk\jdk\src\share\classes\sun\launcher\launcherhelper.java

public enum LauncherHelper {
……
    private static final ClassLoader scloader = ClassLoader.getSystemClassLoader();
……
    public static Class checkAndLoadMain(boolean printToStderr,
                                            int mode,
                                            String what) {
        ……
        mainClass = scloader.loadClass(cn);
        ……

2、\openjdk\jdk\src\share\classes\java\lang\ClassLoader.java

核心代码:sun.misc.Launcher.getLauncher();

    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        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();
        ……

3、\openjdk\jdk\src\share\classes\sun\misc[Launcher.java](https://link.zhihu.com/?target=http%3A//Launcher.java)

核心代码:

  • private static Launcher launcher = new Launcher();
  • extcl = ExtClassLoader.getExtClassLoader();
  • loader = AppClassLoader.getAppClassLoader(extcl);
  • Thread.currentThread().setContextClassLoader(loader);
public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        // Also set the context class loader for the primordial thread.
        Thread.currentThread().setContextClassLoader(loader);
    ……

4、扩展类加载器的创建流程

public static ExtClassLoader getExtClassLoader() throws IOException
        {
       ……
                            return new ExtClassLoader(dirs);
  ……

  public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
        }

   URLClassLoader(URL[] urls, ClassLoader parent,
                   AccessControlContext acc) {

第二个参数传的是null,其实就是parent=null

5、应用类加载器的创建流程

public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException {
    final String s = System.getProperty("java.class.path");
    final File[] path = (s == null) ? new File[0] : getClassPath(s);

    // Note: on bugid 4256530
    // Prior implementations of this doPrivileged() block supplied
    // a rather restrictive ACC via a call to the private method
    // AppClassLoader.getContext(). This proved overly restrictive
    // when loading  classes. Specifically it prevent
    // accessClassInPackage.sun.* grants from being honored.
    //
    return AccessController.doPrivileged(
        new PrivilegedAction() {
            public AppClassLoader run() {
                URL[] urls =
                    (s == null) ? new URL[0] : pathToURLs(path);
                return new AppClassLoader(urls, extcl);
            }
        });
}

AppClassLoader(URL[] urls, ClassLoader parent) {
    super(urls, parent, factory);
}

应用类、扩展类加载器的父子关系就是这样建立的

类加载器加载的类如何存储

image

e. 双亲委派模型

内容:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。每个类加载器的搜索范围上面已经讲过了

image

双亲委派模型的实现:

  • 首先检查类是否被加载
  • 若未加载,则调用父类加载器的loadClass方法
  • 若该方法抛出ClassNotFoundException异常,则表示父类加载器无法加载,则当阿奇类加载器调用findClass加载类
  • 若父类加载器可以加载,则直接返回Class对象

双亲委派模型的好处:保证java类库中的类不受用户类影响,防止用户自定义一个类库中的同名类,引起问题

破坏双亲委派模型:

  • 基础类需要调用用户的代码,解决方式,线程上下文类加载器,也就是父类加载器请求子类加载器去完成了加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。实现方式是重写ClassLoader类的loadClass()。示例:JDBC,原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,JDBC中的Driver类中需要动态加载不同数据库类型的Driver类。还有一种方式是JNDI服务需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码
  • 重写loadClass()方法,双亲委派模型的具体实现就在loadClass()方法中
  • 用户对程序的动态性的追求,例如OSGi(面向Java的动态模型系统,在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。)。代码热替换,模板热部署
  • 典型的打破双亲委派模型的框架和中间件有Tomcat与OSGi
  • 打破双亲委派的意思其实就是不委派、向下委派

模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里的类加载器之间的父子关系一般通过组合(Composition)关系来实现的,而不是通过集成(Inheritance)的关系实现

3. 类加载方式

  • 命令行启动应用的时候由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

4. 类加载时机

  • 通过new,getStatic,putStatic,invokeStatic这四条指令(new一个对象时,调用一个类的静态方法,直接操作一个类的static属性)
  • 实现java.lang.reflect进行反射调用
  • 初始化类时,没有初始化父类,先初始化子类
  • 虚拟机启动时,用户指定的主类(main)

虚拟机把描述类的数据从class文件加载到内存,并对数据进行效验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型

5. 线程上下文类加载器

  1. 是什么

一种特殊的类加载器,可以通过Thread获取,基于此可实现逆向委托加载

  1. 为什么

为了解决双亲委派的缺陷而生

  1. 怎么做
//获取
Thread.currentThread().getContextClassLoader()

// 设置
Thread.currentThread().setContextClassLoader(new Classloader_4());

6. 沙箱安全

跟Linux的权限机制有点像

image

看openjdk源码会看到有这样的判断AccessController.doPrivileged

image

随便搜了一下,35个文件有80处安全判断

image

比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染

你可能感兴趣的:(探索JVM底层之类加载)