java类加载器三种_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文件到内存中呢?

当执行new操作时候,隐式调用类加载其加载对应的类到JVM中,该种方式为隐式装载

当执行Class.forName(“包路径 + 类名”)\ Class.forName(“包路径 + 类名”, ClassLoader)\ ClassLoader.loadClass(“包路径 + 类名”),显式加载需要的类,该种方式为显式装载

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

二、类加载过程

类又是怎么加载的呢?

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

200727926_1_20200830123435661.png

注:在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.引用另一个类中的常量不会触发另一个类的初始化,原因在于“常量传播优化

4使用

使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。当使用阶段完成之后,java类就进入了卸载阶段.下面我们主要来说一下被动引用:

1. 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化

2. 定义类数组,不会引起类的初始化

3. 引用类的常量,不会引起类的初始化

5卸载

关于类的卸载,在类使用完之后,如果满足下面的情况,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找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。即将加载一个类的请求交给父类加载器,加载成功则直接返回,如果找不到或无法加载这个类,则由自己加载。这种机制就叫做双亲委托。

整个流程可以如下图所示:

200727926_2_20200830123435864.png

为什么要双亲委托模型呢?

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类加载器

200727926_3_20200830123436114.png

六、自定义ClassLoader

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

自定义步骤

编写一个类继承自ClassLoader抽象类。

复写它的 findClass() 方法。

在 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对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值