类加载流程

1.Java类的生命周期

加载(loading)
链接(Linking)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initializtion)
使用(Using)
卸载(Unloading)

2.过程一:加载(Loading)

基本数据类型由虚拟机预先定义,不需要类加载器加载,引用数据类型需要类加载器进行类加载。
将Java类的字节码文件加载到内存中,并在内存中构建出Java类的原型— 类模版对象。
类模板对象是JVM中的一个快照,JVM将从class文件中解析出常量池、类字段、类方法等信息,存储到模版中。
加载阶段,查找并加载类的二进制数据,生成Class的实例。
加载类时,JVM需要完成的3件事:
通过类的全名,获取类的二进制数据流。
解析类的二进制数据流为方法区内的数据结构(Java类模型)。
创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口。

3.二进制流的获取方式

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得(只要所读取的字节码符合JVM规范即可—开头ca fe ba be)
虚拟机可能通过文件系统读入一个class文件。
读入jar、zip等归档数据包,提取类文件。
事先存放在数据库中的类的二进制加载。
使用类似于HTTP之类的协议通过网络进行加载(序列化反序列化)
在运行时生成一段class的二进制信息。
获取到的类的二进制信息后,Java虚拟机会处理这些数据,并最终转为一个java.lang.Class的实例。
如果输入数据不是class文件的规范,则会抛出ClassFormatError。

4.过程二:链接----验证

对加载的class文件进行验证。
格式检查
魔数检查
版本检查
长度检查
语义检查
是否继承final
是否有父类
抽象方法是否有实现
字节码验证
跳转指令是否指向正确位置
操作数类型是否合理
符号引用验证(在解析环节执行):Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,在此验证阶段,虚拟机就回检查这些类或者方法确实是存在的。
符号引用的直接引用是否存在。
其中格式验证会和加载阶段一起执行,格式验证之外的验证操作将会在方法区中进行。

5.过程二:链接----准备

为类中的静态变量分配内存,并将其初始化默认值。
注意:
这里不包括基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配,准备阶段会显示赋值,对于引用类型String,如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显示赋值,否则在初始化阶段执行。
不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到Java堆中。
这个阶段不会像初始化阶段中那样会有初始化或者代码被执行。

6.过程二:链接----解析

将类、接口、字段和方法的符号引用转化为直接引用。
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关,Class类文件中包含大量的符号引用,在实际运行中,只有符号引用是不够的,方法被调用时,需要明确该方法的位置。

7.过程三:初始化

初始化阶段,为类的静态变量赋予正确的初始值。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段的重要工作是执行类的初始化方法:()方法:
该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法。
它是由静态成员的赋值语句以及static语句块合并产生的。
在加载类之前,虚拟机总是会试图加载该类的父类,因此父类的总是在子类的之前被调度,口诀:由父及子,静态先行。

8.过程三:初始化—方法

一个类中没有声明任何的类变量,也没有静态代码块。
一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码来执行的初始操作时。
一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式。
public static final String a1 = “1111”; //链接过程准备阶段赋值
public static final String a2 = new String(“111111”); //初始化阶段复制
public static final Integer i1 = 111; //链接过程准备阶段赋值
public static final Integer i2 = new Integer(111111); //初始化阶段复制
public static final Integer i2 = Integer.valueOf(11111); //初始化阶段复制

9.过程三:初始化—线程安全性问题

内存中一个类只能被加载一次,所以类的clinit方法在多线程环境下被加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程需要阻塞等待,直到活动线程执行方法完毕。
之前线程已经成功加载了类,则在队列中的线程就没有机会在执行方法了,虚拟机会直接返回给它已经准备好的信息。
多线程类加载出现死锁的demo。
package com.jvm.classloader;
public class DieLockClassLoaderTest extends Thread{
private String flag;
DieLockClassLoaderTest(String flag){
this.flag = flag;
}
@Override
public void run() {
try {
Class.forName(“com.jvm.classloader.Static”+flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new DieLockClassLoaderTest(“A”).start();
new DieLockClassLoaderTest(“B”).start();
}
}
class StaticA{
static {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Class.forName(“com.jvm.classloader.StaticB”);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(“StaticA 加载完成”);
}
}
class StaticB{
static {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Class.forName(“com.jvm.classloader.StaticA”);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(“StaticB 加载完成”);
}
}

10.类的初始化情况:主动使用 和 被动使用。

类的主动使用会调用方法,被动使用则不会初始化。
主动使用:
当创建一个类的实例,比如new关键字,或者反射、克隆、反序列化。
当调用类的静态方法时,使用了字节码的invokestatic指令。
当使用类、接口的静态字段时(final特殊考虑)。
当使用java.lang.reflect包中的反射方法时,例如:Class.forName。
当初始化子类时,如果发现父类没有被初始化,则需要先初始化父类(如果父类是接口,父类接口不会进行初始化,父接口不会因为它的子接口或者实现类初始化而初始化)。
如果接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,需要在其之前被初始化。
interface CompareA{
public default void method(){
System.out.println(“你好”);
}
}
当虚拟机启动时,用户需要指定一个执行主类(包含main方法的那个类),虚拟机会先初始化主类。

public class DieLockClassLoaderTest{
static {
System.out.println(“初始化加载”);
}
public static void main(String[] args) {
new DieLockClassLoaderTest(“A”).start();
new DieLockClassLoaderTest(“B”).start();
}
}
当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
被动使用:除了以上情况的主动使用,其余情况都属于被动调用,被动调用不会引起类的初始化。
当访问一个静态字段时,只有真正声明这个字段的类才会被初始化(当通过子类引用父类的静态变量,子类不会被初始化)。
通过数据定义类引用,不会触发此类的初始化。
Parent[] parents = new Parent[10];
引用常量不会触发此类或接口的初始化,常量在链接阶段已经被显示的赋值了。
调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化加载。
try {
Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass(“com.jvm.classloader.Son”);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

11.类的卸载

在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得他的类加载器。标识Class实例与其类的加载器之间为双向关联关系。
一个类的实例总是引用代表这个类的Class对象,在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用,此外所有的java类都有一个静态属性class,他引用代表这个类的Class对象。

引导类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
系统类加载器、扩展类加载器的类型在运行不太可能被卸载,其加载的实例在整个运行期间总能够直接或者间接的访问到,其达到unreachable的可能性极小。
只有自定义的类加载器类型在很简单的上下文环境中才能被卸载,一般还需要借助于强制调用虚拟机的垃圾收集功能才可以做到。

12. 类的加载器

ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class实例对象,然后交给Java虚拟机进行链接、初始化等操作。
ClassLoader在整个装载阶段,只能影响到类的加载,无法通过ClassLoader去改变类的链接和初始化行为,至于它是否可以运行,则由Execution Engine(执行引擎)决定。

13.类的加载分类

显式加载:显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
隐式加载是不在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用另一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
Son son = new Son(); //隐式加载
try {
//显示加载
Class.forName(“com.jvm.classloader.Son”);
PassiveUse.class.getClassLoader().loadClass(“com.jvm.classloader.Son”);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

14.类的唯一性和命名空间

类的唯一性:
对于任何一个类,都需要由它的类加载器和这个类本身一同确认其在Java虚拟机的唯一性,比较类相同只有在这两个类是由同一类加载器加载的前提下才有意义。即使这两个类来自同一个Class文件,只要加载的类加载器不相同,那么这两个类就不相同。

命名空间
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有的父类加载器所加载的类组成。
在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
类加载机制特征
双亲委派模型,但不是所有类加载都遵循这个模型。
可见性,子类加载器可以访问父加载器加载的类型,不允许反过来。
单一性,由于父类加载器的类型对于在加载器是可见的,所以父加载器过的类型,就不会在子加载器中重复加载。

15.类加载器分类(-XX:+TraceClassLoading 展示类加载)

JVM支持两种类型的类加载器,分别是引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

不同类加载器子父关系是包含关系,在下层加载器中,包含上层加载器的引用。

类加载器获取
class.getClassLoader() 获取当前类的ClassLoader。
Thread.currentThread().getContextClassLoader() 获取当前线程上下文的ClassLoader。
ClassLoader.getSystemClassLoader() 获取系统的ClassLoader。

16.Class.forName()与ClassLoader.loadClass()方法。

Class.forName()—主动加载:静态方法,该方法将Class文件加载到内存的同时,会执行类的初始化。
ClassLoader.loadClass()—被动加载:是一个实例方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。

17.双亲委派模型-----JDK1.2版本开始

1)优势
避免类的重复加载,确保一个的全局唯一性。
保护程序的安全,防止核心API被篡改。

2)代码支持
双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean) 接口中实现的,接口逻辑如下:
先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
判断当前加载器的父加载器是否为空,如果不为null,则调用parent.loadClass进行加载。
如果父类加载器为空,就回调用findBootstrapClassOrNull接口,让引导类加载器加载。
如果上述三条路径都没有成功加载,则调用findClass接口进行类加载,该接口会调用defineClass的native接口加载java类。
双亲委派模型隐藏在2、3步中。
3)弊端
顶层的ClassLoader无访问底层的ClassLoader所加载的类。

18.双亲委派模式的破坏

自定义类加载器重写loadClass
jdk 1.2之前不存在双亲委派模型
线程上下文类加载器
这是一种父类加载器去请求子类加载类完成加载的行为,解决接口由核心API提供,具体实现是自定类实现。

代码热替换、模块热部署
由于用户对程序动态性的追求而导致的。

19.沙箱安全机制

保证程序安全
保护Java原生的JDK代码。
Java安全模型的核心就是沙箱安全机制,限定在虚拟机(JVM)特定的运行范围中,并严格限制代码对本地系统资源访问。

20.自定义类加载器

隔离加载类
解决类的仲裁—>类冲突
修改类加载的方式
除了Bootstrap外,其他加载类并非一定要引入,可按需加载。
扩展加载源
防止源码泄漏
对类源码进行加密解密操作。

21.自定义类加载器代码

1)实现方式
所有用户自定义类加载器都应该继承ClassLoader类。
重写ClassLoader中:
loadClass()
findClass() ----->推荐
2)对比
loadClass这个方法时实现双亲委派模型逻辑的地方,因此最好在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构
编写好的自定义类加载器,可以用loadClass()方法来实现类加载操作
3)说明
父类加载器是系统类加载器
JVM中所有类加载都会使用java.lang.ClassLoader.loadClass(String) 接口(除自定义加载器重写loadClass方法除外),JDK核心类库也不例外。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值