打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
JVM类加载机制及三种默认类加载器【AppClassLoader/ExtClassLoader/BootstrapClassLoader】

一、ClassLoader

    编译后的Java应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以需要从一个class文件中调用另一个class文件中的方法,而只有class文件被载入到了内存之后,才能被其它class所引用,如果找不到另外一个文件,则会引发系统异常。程序在启动的时候,不会一次性加载所需要的所有class文件到内存中,而是根据需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的。其根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例。其虽然只用于实现类的加载动作,而对于任意一个类,在Java虚拟机中的唯一性都是由加载他的‘类加载器’和其本身一起确立一个独立的类名称空间。即同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么他们的类名称空间就不同,这两个类就必定不相等。

那么JVM什么时候加载.class文件到内存中呢?

  1. 当执行new操作时候,隐式调用类加载其加载对应的类到JVM中,该种方式为隐式装载
  2. 当执行Class.forName(“包路径 + 类名”)\ Class.forName(“包路径 + 类名”, ClassLoader)\ ClassLoader.loadClass(“包路径 + 类名”),显式加载需要的类,该种方式为显式装载

以上情况都会触发类加载器去类加载对应的路径去查找对应的.class文件,并创建Class对象。第二种方式加载字节码到内存后生产的只是一个Class对象,要得到具体的对象实例还需要使用Class对象的newInstance()方法来创建具体实例。

二、类加载过程

类又是怎么加载的呢?

JVM将类的加载过程分为3个步骤:装载(Load)、链接(Link)、初始化(Initialize)

注:在Java 中虚拟机会为每个加载的类维护一个常量池【不同于字符串常量池,这个常量池只是该类的字面值(类名、方法名)和符号引用的有序集合。 而字符串常量池,是整个JVM共享的】。如int a = 5;中的a就是符号引用,而解析过程就是把它转换成指向堆中的对象地址的相对地址。

Java 的类加载过程可以分为3步5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

1 Loading(载入)

    JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存(JVM)中,并生成一个代表该类的 java.lang.Class 对象。该阶段JVM完成3件事: 
    1.通过类的全限定名获取该类的二进制字节流
    2.将字节流所代表的静态存储结构转化为方法区的运行时数据结构
    3.在内存中生成一个该类的java.lang.Class对象,作为该类在方法区的各种数据的访问入口

2 链接

2.1 Verification(验证)
    该步是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,确保二进制字节流格式符合预期(如是否以 cafe bene 开头)、是否所有方法都遵守访问控制关键字的限定、方法调用的参数个数和类型是否正确、确保变量在使用之前被正确初始化了、检查变量是否被赋予恰当类型的值
验证阶段会完成以下4个阶段的检验动作:
    1.文件格式验证:基于字节流验证
    2.元数据验证(是否符合Java语言规范):基于方法区的存储结构验证
    3.字节码验证(确定程序语义合法,符合逻辑):基于方法区的存储结构验证
    4.符号引用验证(确保下一步的解析能正常执行):基于方法区的存储结构验证
2.2 Preparation(准备)
    该步主要为静态变量在方法区分配内存,并设置默认初始值。JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
也就是说,假如有这样一段代码:

public String zhangsan = "张三";public static String lisi = "李四";public static final String wanger= "王二";

zhangsan 不会被分配内存,而 lisi 和wanger会被分配内存。需要注意的是 lisi 的初始值不是“李四”而是 null,而wanger初始值是“王二”。static final 修饰的变量被称作为常量和类变量不同,一旦赋值就不会改变了,所以 wanger 在准备阶段的值为“王二”而不是 null。
2.3 Resolution(解析)
    该步是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程,即将常量池中的符号引用转化为直接引用。
为什么是符号引用,不是直接引用呢,因为符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.One 类引用了 com.Two 类,编译时 One 类并不知道 Two 类的实际内存地址,因此只能使用符号 com.Two。
直接引用通过对符号引用进行解析,找到引用的实际内存地址。

3 Initialization(初始化)

    该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
父类和子类的调用关系:先加载父类的静态变量,然后是子类的静态变量。当new一个子类时,加载父类的非静态变量,然后是父类的构造函数,再然后是子类的非静态变量和子类的构造函数
以下三种引用类的方式不会触发初始化(也就是类的加载):
  1.通过子类引用父类的静态字段,不会导致子类初始化
  2.通过数组定义来引用类,不会触发此类的初始化
  3.引用另一个类中的常量不会触发另一个类的初始化,原因在于“常量传播优化

使用

 使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。当使用阶段完成之后,java类就进入了卸载阶段.下面我们主要来说一下被动引用:
    1. 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化
    2. 定义类数组,不会引起类的初始化
    3. 引用类的常量,不会引起类的初始化

卸载

  关于类的卸载,在类使用完之后,如果满足下面的情况,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了
  1.该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
  2.加载该类的ClassLoader已经被回收
  3.该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

类进行初始化时机

    类的初始化时机,虚拟机规范指明,有且只有五种情况必须立即对类进行初始化(而该过程自然发生在加载、验证、准备之后)
  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:
  注意:newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类Ljava.lang.String的初始化,而不会触发String类的初始化
    1.1 使用new关键字实例化对象的时候
      当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化,即有父类当new方法创建一个子类对象时,先初始化父类的静态变量和静态代码块,再初始化子类的静态变量和静态代码块,然后在初始父类的非静态变量、非静态代码块和构造方法,最后初始化子类的非静态变量、非静态代码块和构造方法
      注:1. 接口除外,父接口在调用的时候才会被初始化,2.子类引用父类静态字段,只会引发父类初始化
    1.2 读取或设置一个类的静态字段的时候(被final修饰的常量字段,在编译器时就把结果放入常量池的静态字段除外)
      有父类当子类调用父类或子类的常量时,不初始化因为该字段在在编译器时就把结果放入常量池中了
      有父类当子类调用父类的静态变量时,只初始父类的静态变量和静态代码块
      有父类当子类调用自己的静态变量时,先初始父类的静态变量和静态代码块,再进行子类的静态变量和静态代码块初始化
    1.3 调用一个类的静态方法的时候
      有父类当子类调用父类的静态方法时,只初始父类的静态变量和静态代码块
      有父类当子类调用自己的静态方法时,先初始父类的静态变量和静态代码块,再进行子类的静态变量和静态代码块初始化
  2. 使用java.lang.reflect包的Class.forName(“xxxx”)方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  3. 被标明为启动类的类(即包含main()方法的类),启动时虚拟机会先初始化该主类
  5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化

例:

String wanger = new String("王二");

上面这段代码使用了 new 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 wanger 进行实例化。

三、ClassLoader并发加载

    JDK 6中loadClass方法是同步的,且同步操作修饰在方法级别,其意味着同步锁将在classLoader实例上。
        多个自定义的classLoader可以继承不同的和相同的父亲加载器,甚至导致交叉。如CL1将CL2作为parent,CL2也可以将CL1作为parent。本质上这种设计思想上,并没有错。
    因为双亲委派关系,该委派方式在全局来看,并没有遵守“单向、层级的”委派关系。
        CLoad1、CLoad2两个加载器的实例(通常一种类加载器只有一个实例),分别在两个(或者多个)线程中并发的load类时,死锁可能就会发生。
    因为loadClass方法是同步的,所以Thread1使用CLoad1加载类时首先获取CLoad1实例锁、loadClass方法中还需要获取CLoad2的锁(调用CLoad2的loadClass),那么Thread2使用CLoad2加载类也出现相同的问题,只是对象锁的顺序不同。这样就会导致死锁问题。
    JDK 1.7中 移除了loadClass方法级别的同步,其在内部设计了一个私有的静态容器concurrentHashMap来解决并发问题,在这个容器中Key为className(全限定名),Value为Object对象锁。在执行loadClass时,首先检测容器中是否已经有此className的对象锁,有则根据此对象锁进行同步,没有则创建。简单而言,就是降低了锁的粒度。此后,即使交叉委派,也不会再出现死锁的问题。
     在jdk1.7中默认的三种加载器都已经实现并行加载,但用户自定义加载器若需要并行加载,需要自行配置,通过调用registerAsParallelCapable()
     JDK并没有默认开启并发加载特性,我们需要关注一个方法:
        protected static boolean registerAsParallelCapable()
        此方法在JDK 7新增,表示将此classLoader注册为“并发的”;只有调用了此方法,classLoader中的concurrentHashMap才会被初始化,即支持className级别的并发锁.
        否则(map == null)仍然将classLoader实例作为同步对象。
    此方法,需要在所有的自定义ClassLoader中都要调用(包括委派的parent是自定义时),否则将不会生效

小结

1、加载阶段是将class加载到内存的过程,即根据类的全限定名查找定义此类的二进制字节流,将静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表该类的Class对象,作为方法区数据的访问入口
2、连接阶段先对文件格式、元数据、字节码符号引用进行验证,保证加载进来的字节流符合JVM规范,然后为静态变量在方法区分配内存,并设置默认值(int:0、Long:0L、String:null、Boolean:false),常量直接进行初始化赋值,将常量池中的符号引用替换成直接饮用
3、初始化阶段该阶段把类变量赋值期望值,即执行类构造器方法的过程,
  初始化顺序(先父类,后子类):
    1. 父类静态变量在静态代码块
    2. 子类静态变量在静态代码块
    3. 父类非静态变量在非静态代码块最后构造方法
    4. 子类非静态变量在非静态代码块最后构造方法

四、默认类加载器

BootstrapClassLoader(启动类加载器)

该类加载器是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,即将‘\lib’目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的,是虚拟机自身的一部分。

注意 启动jvm时可以指定-Xbootclasspath和路径来改变BootstrapClassLoader的加载目录。如java -Xbootclasspath/c:path 被指定的文件追加到默认的bootstrap路径中。

ExtClassLoader(扩展类加载器)

    该类加载器主要负责主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包,当然也可以加载由java.ext.dirs系统属性指定的jar包,用来加载java的扩展库,开发者可以直接使用这个类加载器(其是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)。

AppClassLoader(应用程序类加载器)

    该类加载器负责加载负责在JVM启动时,加载用户类路径(CLASSPATH)下的类库,即来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。一般情况下该类加载就是系统默认的类加载器。

五、类加载器的机制(约束)

委托(双亲委托)

    类加载器查找class和resource时,是通过“委托模式”进行的,其先判断该class是否已经加载成功,当没加载成功时它并不是自己去进行查找,而是先通过父加载器,然后递归下去,直到BootstrapClassLoader,如果BootstrapClassloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。即将加载一个类的请求交给父类加载器,加载成功则直接返回,如果找不到或无法加载这个类,则由自己加载。这种机制就叫做双亲委托。
整个流程可以如下图所示:  

为什么要双亲委托模型呢?
1、其使类加载器具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。保证了一个类只会加载一次,之后这个类的信息放在堆空间,静态属性放在方法区
2、考虑到安全因素,防止核心api中定义类型不会被随意替换。如传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

可见性

可见性指子加载器可以看到父加载器加载的类,而父加载器看不到子加载器加载的类

单一性

单一性指个类仅加载一次,双亲委托机制确保子类加载器不会再次加载父类加载器加载过的类。

破坏双亲委派模型

    双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。
若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码时,就需要破坏双亲委派模型了。

JNDI破坏双亲委派模型

    JNDI是Java标准服务,它的代码由启动类加载器去加载,但JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI),为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型

Spring破坏双亲委派模型

    Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。
那么Spring是如何访问WEB-INF下的用户程序呢?
使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。
利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

Tomcat类加载架构

Tomcat目录下有4组目录:
/common目录下:类库可以被Tomcat和Web应用程序共同使用;由 Common ClassLoader类加载器加载目录下的类库
/server目录:类库只能被Tomcat可见;由 Catalina ClassLoader类加载器加载目录下的类库
/shared目录:类库对所有Web应用程序可见,但对Tomcat不可见;由 Shared ClassLoader类加载器加载目录下的类库
/WebApp/WEB-INF目录:仅仅对当前web应用程序可见。由 WebApp ClassLoader类加载器加载目录下的类库
每一个JSP文件对应一个JSP类加载器

六、自定义ClassLoader

当我们需要动态加载一些东西呢,如从D盘某个文件夹加载一个class文件或从网络上下载class主内容然后再进行加载。我们需要我们自定义一个classloader。

自定义步骤

  1. 编写一个类继承自ClassLoader抽象类。
  2. 复写它的 findClass() 方法。
  3. 在 findClass() 方法中调用defineClass()。  

defineClass()

该方法非常重要,它能将class二进制内容转换成Class对象,当不符合要求时会抛出各种异常。

注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 这样就能够保证它能访问系统内置加载器加载成功的class文件。

自定义ClassLoader示例

我们定义一个MyClassLoader自定义加载器,默认加载路径为 D:\lib 下的jar包和资源。 

import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;public class MyClassLoader extends ClassLoader {    private String mLibPath;    public MyClassLoader() {        mLibPath = "D:\\lib";    }    public MyClassLoader(String path) {        mLibPath = path;    }    //获取要加载的Class     @Override    protected Class<?> findClass(String name) throws ClassNotFoundException {        String fileName = getFileName(name);        File file = new File(mLibPath,fileName);        try {            FileInputStream is = new FileInputStream(file);            ByteArrayOutputStream bos = new ByteArrayOutputStream();            int len = 0;            try {                while ((len = is.read()) != -1) {                    bos.write(len);                }            } catch (IOException e) {                e.printStackTrace();            }            byte[] data = bos.toByteArray();            is.close();            bos.close();            return defineClass(name,data,0,data.length);        } catch (IOException e) {            e.printStackTrace();        }        return super.findClass(name);    }    //获取要加载的class文件名    private String getFileName(String name) {        int index = name.lastIndexOf('.');        if(index == -1){             return name+".class";        }else{            return name.substring(index)+".class";        }    }}

在自定义加载器中,我们在 findClass() 方法中定义了查找class的方法,然后数据通 defineClass() 方法生成了Class对象。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
深入Java虚拟机JVM类加载初始化学习笔记
超详细java类加载器学习总结
java类加载器总结
Java基础知识的27个点
JVM-类加载
Java程序员从笨鸟到菜鸟之(九十三)深入java虚拟机(二)——类加载器详解(上)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服