《深刻理解JVM》 虚拟机类加载机制

1、类加载过程

一、加载

加载是文件到内存的过程。完成如下3件事:html

  • 经过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个表明这个类的Java.lang.class对象,做为方法区这个类的各类数据的访问入口。

二、验证

验证是对类文件内容验证。目的在于确保Class文件符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括如下四种:java

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

三、准备

准备阶段是进行内存分配。为类变量也就是类中由static修饰的变量分配内存,而且设置初始值,这里要注意,初始值是0或者null,而不是代码中设置的具体值,代码中设置的值是在初始化阶段完成的。另外这里也不包含用final修饰的静态变量,由于final在编译的时候就会分配了。算法

四、解析

解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量等。注意,这个阶段不必定要有。api

五、初始化

初始化主要完成静态块执行与静态变量的赋值。这是类加载最后阶段,若被加载类的父类没有初始化,则先对父类进行初始化。缓存

2、什么是Classloader

一个Java程序要想运行起来,首先须要通过编译生成 .class文件,而后建立一个运行环境(jvm)来加载字节码文件到内存运行,而.class 文件是怎样被加载中jvm中的就是Java Classloader所作的事情。安全

那么.class文件何时会被类加载器加载到jvm中运行呢?好比执行new操做时候,当咱们使用Class.forName(“包路径+类名”)、Class.forName(“包路径+类名”,initialize,classloader)、ClassLoader.loadclass(“包路径+类名”);时候就触发了类加载器去类加载对应的路径去查找*.class,并建立Class对象。数据结构

Class.forName():将类的.class文件加载到jvm中以外,还会对类进行解释并执行类中的static块;jvm

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。函数

:Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。而且只有调用了newInstance()方法采用调用构造函数,建立类的对象 。this

3、类加载器

如何寻找类加载器先来看个例子:

public class ClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);// 应用类加载器
        System.out.println(loader.getParent());// 扩展类加载器
        System.out.println(loader.getParent().getParent());
    }

}

运行后,输出结果:

sun.misc.Launcher$AppClassLoader@56a96eba
sun.misc.Launcher$ExtClassLoader@da4a1c9
null

从上面的结果能够看出,并无获取到ExtClassLoader的父Loader,缘由是Bootstrap ClassLoader(引导类加载器)是用C语言实现的,找不到一个肯定的返回父Loader的方式,因而就返回null。

这几种类加载器的层次关系以下图所示:

注意:这里父类加载器并非经过继承关系来实现的,而是采用组合实现的。通常咱们都认为ExtClassloader的父类加载器是BootStarpClassloader,可是其实他们之间根本是没有父子关系的,只是在ExtClassloader找不到要加载类时候会去委托BootStrap加载器去加载。

站在Java虚拟机的角度来说,只存在两种不一样的类加载器:

  1. 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5以后默认的虚拟机,有不少其余的虚拟机是用Java语言实现的),是虚拟机自身的一部分;
  2. 全部其余的类加载器:这些类加载器都由Java语言实现,独立于虚拟机以外,而且所有继承自抽象类java.lang.ClassLoader,这些类加载器须要由启动类加载器加载到内存中以后才能去加载其余的类。

站在Java开发人员的角度来看,类加载器能够大体划分为如下三类:

3.1 BootstrapClassloader

引导类加载器,又称启动类加载器,是最顶层的类加载器,主要用来加载Java核心类,如rt.jar、resources.jar、charsets.jar等,Sun的JVM中,执行java的命令中使用-Xbootclasspath选项或使用- D选项指定sun.boot.class.path系统属性值能够指定附加的类,它不是 java.lang.ClassLoader的子类,而是由JVM自身实现的该类c 语言实现,Java程序访问不到该加载器。

经过下面代码能够查看该加载器加载了哪些jar包:

package test;

import java.net.URL;

public class ClassLoaderTest {

    public static void main(String[] args) {       
        getJars();
    }
    
    public static void getJars() {  
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();    
        for (int i = 0; i < urls.length; i++) {    
            System.out.println(urls[i].toExternalForm());    
        }   
    }

}

执行结果:

file:/Z:/JDK/jre/lib/resources.jar
file:/Z:/JDK/jre/lib/rt.jar
file:/Z:/JDK/jre/lib/sunrsasign.jar
file:/Z:/JDK/jre/lib/jsse.jar
file:/Z:/JDK/jre/lib/jce.jar
file:/Z:/JDK/jre/lib/charsets.jar
file:/Z:/JDK/jre/lib/jfr.jar
file:/Z:/JDK/jre/classes

写到这里你们应该都知道,咱们并无在classpath里面指定这些类的路径,为啥仍是能被加载到jvm并使用起来了吧,由于这些是bootstarp来加载的。

3.2 ExtClassloader

扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的全部jar包或者由java.ext.dirs系统属性指定的jar包。放入这个目录下的jar包对全部AppClassloader都是可见的(后面会知道ExtClassloader是AppClassloader的父加载器)。那么ext都是在哪些地方加载类呢?

System.out.println(System.getProperty("java.ext.dirs"));

3.3 AppClassloader

系统类加载器,又称应用加载器,本文说的SystemClassloader和APPClassloader是一个东西,它负责在JVM启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操做系统属性所指定的JAR类包和类路径。调用ClassLoader.getSystemClassLoader()能够获取该类加载器。若是没有特别指定,则用户自定义的任何类加载器都将该类加载器做为它的父加载器,这点经过ClassLoader的无参构造函数能够知道以下:

protected ClassLoader() {
   this(checkCreateClassLoader(), getSystemClassLoader());
}

执行如下代码便可得到classpath加载路径:

System.out.println(System.getProperty("java.class.path"));

3.4 双亲委派模型

Java类加载器使用的是双亲委派模型,若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。

一、当AppClassLoader加载一个class时,它首先不会本身去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

二、当ExtClassLoader加载一个class时,它首先也不会本身去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

三、若是BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

四、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,若是AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

那么问题来了,为啥使用这种方式呢?由于这样能够避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,咱们试想一下,若是不使用这种委托模式,那咱们就能够随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在很是大的安全隐患,而双亲委托的方式,就能够避免这种状况,由于String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,因此用户自定义的ClassLoader永远也没法加载一个本身写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

双亲委派模型意义

一、系统类防止内存中出现多份一样的字节码,避免类的重复加载;

二、避免Java的核心API被篡改,保证Java程序安全稳定运行。

下面咱们从源码看如何实现双亲委派模型:

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;
        }
    }

分析代码知道:

  1. 首先从jvm缓存查找该类,若是该类以前被加载过,则直接从jvm缓存返回该类,否者看当前类加载器是否有父加载器;
  2. 若是有父加载器的话则委托为父类加载器进行加载,否者委托BootStrapClassloader进行加载;
  3. 若是仍是没有找到,则调用当前Classloader的findclass方法进行查找。

从上面源码知道要想修改类加载委托机制,实现本身的载入策略能够经过覆盖ClassLoader的findClass方法或者覆盖loadClass方法来实现。

Java应用启动过程是首先Bootstarp Classloader加载rt.jar包里面的sun.misc.Launcher类,而该类内部使用BootstarpClassloader加载器构建和初始化Java中三种类加载和线程上下文类加载器,而后在根据不一样场景去使用这些类加载器去本身的类查找路径去加载类。

参考

http://ifeve.com/classloader%e8%a7%a3%e6%83%91/

http://www.cnblogs.com/ityouknow/p/5603287.html

相关文章
相关标签/搜索