阿里P7架构师对Java虚拟机、类加载机制是怎么理解的?

阿里P7架构师对Java虚拟机、类加载机制是怎么理解的?

 

概述

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载java

(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化程序员

(Initialization)、使用(Using)和卸载(Unloading)7 个阶段。其中验证、准备、解析web

3 个部分统称为链接(Linking)bootstrap

于初始化阶段,虚拟机规范则是严格规定了有且只有 5 种状况必须当即对类进行“初始api

化”(而加载、验证、准备天然须要在此以前开始):tomcat

1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,若是类没有进行安全

过初始化,则须要先触发其初始化。生成这 4 条指令的最多见的 Java 代码场景是:使用数据结构

new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译期多线程

把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。架构

2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,若是类没有进行过初始化,

则须要先触发其初始化。

3)当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类

的初始化。

4)当虚拟机启动时,用户须要指定一个要执行的主类(包含 main()方法的那个类),

虚拟机会先初始化这个主类。

5)当使用 JDK 1.7 的动态语言支持时,若是一个 java.lang.invoke.MethodHandle 实例最

后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,而且这个

方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。

注意:

对于静态字段,只有直接定义这个字段的类才会被初始化,所以经过其子类来引用父类中

定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

常量 HELLOWORLD,但其实在编译阶段经过常量传播优化,已经将此常量的值“hello

world”存储到了 NotInitialization 类的常量池中,之后 NotInitialization 对常量

ConstClass.HELLOWORLD 的引用实际都被转化为 NotInitialization 类对自身常量池的引

用了。

也就是说,实际上 NotInitialization 的 Class 文件之中并无 ConstClass 类的符号引用入

口,这两个类在编译成 Class 以后就不存在任何联系了。

加载阶段

虚拟机须要完成如下 3 件事情:

1)经过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个表明这个类的 java.lang.Class 对象,做为方法区这个类的各类数据

的访问入口。

验证

是链接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合

当前虚拟机的要求,而且不会危害虚拟机自身的安全。但从总体上看,验证阶段大体上会

完成下面 4 个阶段的检验动做:文件格式验证、元数据验证、字节码验证、符号引用验

证。

准备阶段

是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法

区中进行分配。这个阶段中有两个容易产生混淆的概念须要强调一下,首先,这时候进行

内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在

对象实例化时随着对象一块儿分配在 Java 堆中。其次,这里所说的初始值“一般状况”下

是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量 value 在准备阶段事后的初始值为 0 而不是 123,由于这时候还没有开始执行任何

Java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器<

clinit>()方法之中,因此把 value 赋值为 123 的动做将在初始化阶段才会执行。表 7-1

列出了 Java 中全部基本数据类型的零值。

假设上面类变量 value 的定义变为:public static final int value=123;

编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据

ConstantValue 的设置将 value 赋值为 123。

解析阶段

是虚拟机将常量池内的符号引用替换为直接引用的过程

类初始化阶段

是类加载过程的最后一步,前面的类加载过程当中,除了在加载阶段用户应用程序能够经过

自定义类加载器参与以外,其他动做彻底由虚拟机主导和控制。到了初始化阶段,才真正

开始执行类中定义的 Java 程序代码在准备阶段,变量已经赋过一次系统要求的初始值,

而在初始化阶段,则根据程序员经过程序制定的主观计划去初始化类变量和其余资源,或

者能够从另一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。<

clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static{}

块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

<clinit>()方法对于类或接口来讲并非必需的,若是一个类中没有静态语句块,也没

有对变量的赋值操做,那么编译器能够不为这个类生成<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,若是多个

线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其余

线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是在一个类的<clinit

>()方法中有耗时很长的操做,就可能形成多个进程阻塞。

阿里P7架构师对Java虚拟机、类加载机制是怎么理解的?

 

类加载器

如何自定义类加载器,看代码

系统的类加载器

对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在 Java 虚拟机中

的惟一性,每个类加载器,都拥有一个独立的类名称空间。这句话能够表达得更通俗一

些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意

义,不然,即便这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们

的类加载器不一样,那这两个类就一定不相等。

这里所指的“相等”,包括表明类的 Class 对象的 equals()方法、isAssignableFrom

()方法、isInstance()方法的返回结果,也包括使用 instanceof 关键字作对象所属关

系断定等状况。

在自定义 ClassLoader 的子类时候,咱们常见的会有两种作法,一种是重写 loadClass 方

法,另外一种是重写 findClass 方法。其实这两种方法本质上差很少,毕竟 loadClass 也会

调用 findClass,可是从逻辑上讲咱们最好不要直接修改 loadClass 的内部逻辑。我建议的

作法是只在 findClass 里重写自定义类的加载方法。

loadClass 这个方法是实现双亲委托模型逻辑的地方,擅自修改这个方法会致使模型被破

坏,容易形成问题。所以咱们最好是在双亲委托模型框架内进行小范围的改动,不破坏原

有的稳定结构。同时,也避免了本身重写 loadClass 方法的过程当中必须写双亲委托的重复

代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。

双亲委派模型

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

(Bootstrap ClassLoader),这个类加载器使用 C++语言实现,是虚拟机自身的一部分;

另外一种就是全部其余的类加载器,这些类加载器都由 Java 语言实现,独立于虚拟机外

部,而且全都继承自抽象类 java.lang.ClassLoader。

启动类加载器(Bootstrap ClassLoader):这个类将器负责将存放在<JAVA_HOME>\lib

目录中的,或者被-Xbootclasspath 参数所指定的路径中的,而且是虚拟机识别的(仅按照

文件名识别,如 rt.jar,名字不符合的类库即便放在 lib 目录中也不会被加载)类库加载到

虚拟机内存中。启动类加载器没法被 Java 程序直接引用,用户在编写自定义类加载器

时,若是须要把加载请求委派给引导类加载器,那直接使用 null 代替便可。

扩展类加载器(Extension ClassLoader):这个加载器由

sun.misc.Launcher$ExtClassLoader 实现,它负责加载<JAVA_HOME>\lib\ext 目录中

的,或者被 java.ext.dirs 系统变量所指定的路径中的全部类库,开发者能够直接使用扩展

类加载器。

应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher

$App-ClassLoader 实现。因为这个类加载器是 ClassLoader 中的 getSystemClassLoader

()方法的返回值,因此通常也称它为系统类加载器。它负责加载用户类路径

(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没

有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

咱们的应用程序都是由这 3 种类加载器互相配合进行加载的,若是有必要,还能够加入自

己定义的类加载器。

双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有本身的父类加载

器。这里类加载器之间的父子关系通常不会以继承(Inheritance)的关系来实现,而是都

使用组合(Composition)关系来复用父加载器的代码。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着

它的类加载器一块儿具有了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在

rt.jar 之中,不管哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类

加载器进行加载,所以 Object 类在程序的各类类加载器环境中都是同一个类。相反,若是

没有使用双亲委派模型,由各个类加载器自行去加载的话,若是用户本身编写了一个称为

java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不一样的

Object 类,Java 类型体系中最基础的行为也就没法保证,应用程序也将会变得一片混乱。

Tomcat 类加载机制

Tomcat 自己也是一个 java 项目,所以其也须要被 JDK 的类加载机制加载,也就必然存在

引导类加载器、扩展类加载器和应用(系统)类加载器。

Common ClassLoader 做为 Catalina ClassLoader 和 Shared ClassLoader 的 parent,而

Shared ClassLoader 又可能存在多个 children 类加载器 WebApp ClassLoader,一个

WebApp ClassLoader 实际上就对应一个 Web 应用,那 Web 应用就有可能存在 Jsp 页

面,这些 Jsp 页面最终会转成 class 类被加载,所以也须要一个 Jsp 的类加载器。

须要注意的是,在代码层面 Catalina ClassLoader、Shared ClassLoader、Common

ClassLoader 对应的实体类实际上都是 URLClassLoader 或者 SecureClassLoader,通常

咱们只是根据加载内容的不一样和加载父子顺序的关系,在逻辑上划分为这三个类加载器;

而 WebApp ClassLoader 和 JasperLoader 都是存在对应的类加载器类的。

当 tomcat 启动时,会建立几种类加载器:

1 Bootstrap 引导类加载器 加载 JVM 启动所需的类,以及标准扩展类(位于 jre/lib/ext

下)

2 System 系统类加载器 加载 tomcat 启动的类,好比 bootstrap.jar,一般在 catalina.bat

或者 catalina.sh 中指定。位于 CATALINA_HOME/bin 下。

3 Common 通用类加载器 加载 tomcat 使用以及应用通用的一些类,位于

CATALINA_HOME/lib 下,好比 servlet-api.jar

4 webapp 应用类加载器每一个应用在部署后,都会建立一个惟一的类加载器。该类加载器

会加载位于 WEB-INF/lib 下的 jar 文件中的 class 和 WEB-INF/classes 下的 class 文件。

方法调用详解

解析

调用目标在程序代码写好、编译器进行编译时就必须肯定下来。这类方法的调用称为解

析。

在 Java 语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法

和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特

点决定了它们都不可能经过继承或别的方式重写其余版本,所以它们都适合在类加载阶段

进行解析。

静态分派

多见于方法的重载。

阿里P7架构师对Java虚拟机、类加载机制是怎么理解的?

 

“Human”称为变量的静态类型(Static Type),或者叫作的外观类型(Apparent

Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在

程序中均可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量自己的静态

类型不会被改变,而且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行

期才可肯定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

代码中定义了两个静态类型相同但实际类型不一样的变量,但虚拟机(准确地说是编译器)

在重载时是经过参数的静态类型而不是实际类型做为断定依据的。而且静态类型是编译期

可知的,所以,在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪一个重载版

本,因此选择了 sayHello(Human)做为调用目标。全部依赖静态类型来定位方法执行版

本的分派动做称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶

段,所以肯定静态分派的动做实际上不是由虚拟机来执行的。

动态分派

静态类型一样都是 Human 的两个变量 man 和 woman 在调用 sayHello()方法时执行了

不一样的行为,而且变量 man 在两次调用中执行了不一样的方法。致使这个现象的缘由很明

显,是这两个变量的实际类型不一样。

在实现上,最经常使用的手段就是为类在方法区中创建一个虚方法表。虚方法表中存放着各个

方法的实际入口地址。若是某个方法在子类中没有被重写,那子类的虚方法表里面的地址

入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。若是子类中重写了这

个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。PPT 图中,Son

重写了来自 Father 的所有方法,所以 Son 的方法表没有指向 Father 类型数据的箭头。但

是 Son 和 Father 都没有重写来自 Object 的方法,因此它们的方法表中全部从 Object 继承

来的方法都指向了 Object 的数据类型。

基于栈的字节码解释执行引擎

Java 编译器输出的指令流,基本上]是一种基于栈的指令集架构,指令流中的指令大部分

都是零地址指令,它们依赖操做数栈进行工做。与

基于寄存器的指令集,最典型的就是 x86 的二地址指令集,说得通俗一些,就是如今咱们

主流 PC 机中直接支持的指令集架构,这些指令依赖寄存器进行工做。

举个最简单的例子,分别使用这两种指令集计算“1+1”的结果,基于栈的指令集会是这

样子的:

iconst_1

iconst_1

iadd

istore_0

两条 iconst_1 指令连续把两个常量 1 压入栈后,iadd 指令把栈顶的两个值出栈、相加,然

后把结果放回栈顶,最后 istore_0 把栈顶的值放到局部变量表的第 0 个 Slot 中。

若是基于寄存器,那程序可能会是这个样子:

mov eax,1

add eax,1

mov 指令把 EAX 寄存器的值设为 1,而后 add 指令再把这个值加 1,结果就保存在 EAX

寄存器里面。

基于栈的指令集主要的优势就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件

寄存器则不可避免地要受到硬件的约束。栈架构指令集的主要缺点是执行速度相对来讲会

稍慢一些。全部主流物理机的指令集都是寄存器架构也从侧面印证了这一点。

阿里P7架构师对Java虚拟机、类加载机制是怎么理解的?

相关文章
相关标签/搜索