深入理解java虚拟机之类加载机制

回顾我们《深入理解java虚拟机之初识JVM》,我们了解了类加载的过程,我们知道了类加载的生命周期有以下七个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中解析和初始化交换顺序可以实现动态绑定。
在这里插入图片描述
类初始化的时机:(类初始化的时候加载验证准备也会随着发生)
JVM规范中严格规定了五种情况下必须对类进行初始化:

1.遇到 new 、get static、put static(被final修饰、已在编译器把结果放入常量池的静态字段除外)、invoke static(调用一个类的静态方法)这四条字节码指令时;
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则先触发其初始化;
3.父类未被初始化;
4.JVM启动时,用户指定的主类(包含main方法的类)要被初始化;
5.使用JDK1.7动态语言支持的时候,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

以上五种就是对一个类的主动引用,其他不会触发初始化的被称为被动引用:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
    System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
  2. 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
    SuperClass[] sca = new SuperClass[10];
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
    System.out.println(ConstClass.HELLOWORLD);

类加载的过程(加载 验证 准备 解析 初始化)
1.加载:主要完成以下三件事:
## 通过一个类的全限定名获取此类的二进制字节流;
## 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构;
## 在内存()中生成一个代表这个类的class对象,作为方法区这个类的各种数据的访问入口。

其中二进制字节流可以从以下方式中获取:
##从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
##从网络中获取,这种场景最典型的应用是 Applet。
##运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
##由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
##从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

2.验证
确保Class文件的字节流中包含的信息符合当前JVM的要求,且不会危害虚拟机自身的安全。
包括:文件格式验证、元数据验证(对字节码描述的信息进行语义分析)、字节码验证(通过数据流和控制流分析,确保程序语义是合法、符合逻辑的,将对类的方法体进行校验分析)、符号引用验证。

3.准备
类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这个阶段分配内存,它会在对象实例化的时候随着对象一起分配在java堆中。

4.解析
将常量池的符号引用替换为直接引用的过程。

5.初始化
初始化阶段即虚拟机执行类构造器clinit() 方法的过程。对类的静态方法、静态变量和静态代码块初始化的过程。

在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

clinit() 方法具有以下特点:
是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
例如以下代码:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
与类的构造函数(或者说实例构造器 init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 clinit>() 方法运行之前,父类的 clinit>() 方法已经执行结束。因此虚拟机中第一个执行 clinit>() 方法的类肯定为 java.lang.Object。

由于父类的 clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。

clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 clinit>() 方法。

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 clinit>() 方法。但接口与类不同的是,执行接口的 clinit>() 方法不需要先执行父接口的 clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 clinit>() 方法。
虚拟机会保证一个类的 clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 clinit>() 方法完毕。如果在一个类的 clinit>() 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。

类与类加载器

什么是类加载器:
负责读取java字节代码,并转化为java.lang.class类的一个实例。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof() 关键字对做对象所属关系判定等情况),只有在这两个类时由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

1.从虚拟机角度看,主要是两种类加载器:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
另一种就是所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

2.从java开发者的角度看:

a.启动类加载器(Bootstrap ClassLoader)
此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用 null 代替即可。???

b.扩展类加载器(Extension ClassLoader)(JDK9 改为了PlateForm ClassLoader)
这个类加载器是由 ExtClassLoader实现的。它负责将 <Java_Home>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

c.应用程序类加载器(Application ClassLoader)
这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载器的双亲委派模型(Parents Delegation Model)

应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。
该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过**组合(Composition)关系来实现,而不是通过继承(Inheritance)**的关系实现。

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。(如果父层已经加载过该类,就返回该类的引用)

优势

使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类 java.lang.Object,它存放再 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为`java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。
在这里插入图片描述

其实双亲委派模型并不一定都适用所有场景,所以出现了OSGI采用网络图状的类加载模型,各个模块之间互相委托,每一个bundle(模块)之间拥有自己的类资源库和classpath。就打破了双亲委派模型,程序的动态性更高。

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值