1.1 Java及JVM简介

TIOBE语言热度排行榜:https://www.tiobe.com/tiobe-index/

世界上没有最好的编程语言,如果有,我相信一定是JAVA。

1.2 Java发展的重大事件

1995年5月23日,Java语言诞生

1996年1月,第一个JDK-JDK1.0诞生

1996年4月,10个最主要的操作系统供应商申明将在其产品中嵌入JAVA技术

1996年9月,约8.3万个网页应用了JAVA技术来制作

1997年2月18日,JDK1.1发布

1997年4月2日,JavaOne会议召开,参与者逾一万人,创当时全球同类会议规模之纪录

1997年9月,JavaDeveloperConnection社区成员超过十万

1998年2月,JDK1.1被下载超过2,000,000次

1998年12月8日,JAVA2企业平台J2EE发布

1999年6月,SUN公司发布Java的三个版本:标准版(J2SE)、企业版(J2EE)和微型版(J2ME)

2000年5月8日,JDK1.3发布

2000年5月29日,JDK1.4发布

2001年6月5日,NOKIA宣布,到2003年将出售1亿部支持Java的手机

2001年9月24日,J2EE1.3发布

2002年2月26日,J2SE1.4发布,自此Java的计算能力有了大幅提升

2004年9月30日18:00PM,J2SE1.5发布,成为Java语言发展史上的又一里程碑。为了表示该版本的

重要性,J2SE1.5更名为Java SE 5.0

2005年6月,JavaOne大会召开,SUN公司公开Java SE 6。此时,Java的各种版本已经更名,以取消其中的数字“2”:J2EE更名为Java EE,J2SE更名为Java SE,J2ME更名为Java ME

2006年12月,SUN公司发布JRE6.0

2010年9月,JDK7.0已经发布,增加了简单闭包功能。

1.3 学习JVM的动力

我们为什么要学习JVM呢?我们来看一下官方解释:

Java官网 :https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-1.html#jvms-1.2

1.Java虚拟机是Java平台的基石,其负责其硬件和操作系统的独立性,其编译的代码很小以及保护

用户免受恶意程序攻击的能力。

2.Java虚拟机是一种抽象计算机,像真正的计算机一样,它有一个指令集并在运行时操作各种内

存区域。

3.Java虚拟机不承担任何特定的实现技术、主机硬件或主机操作系统,它本身并没有被解释。

4.Java虚拟机不知道Java编程语言,只知道特定的二进制格式,即 class 文件格式, class 文件

包含Java虚拟机指令(或字节码)和符号表,以及其他辅助信息。

5.出于安全考虑,Java虚拟机对 class 文件中的代码施加了强大的语法和结构约束,但是,任何

具有可以用有效 class 文件表示的功能的语言都可以由Java虚拟机托管,由通用的、与机器无关

的平台吸引,其他语言的实现者可以将Java虚拟机作为其语言的交付工具。

这是我针对上述语言精炼的一些翻译。这些理由就足以让一部分同学深入的学习JVM了。

但是上述这些理由可能都不足以让我们所有的同学信服,因为现在大部分的攻城狮以及程序猿可能更关注的是跟我们相关的一些问题,那么在这里我就换一种方式让你接受我们的JVM。

  • 如果你在线上遇到了OOM,你是否会束手无策。
  • 线上卡顿是否可能是因为频繁Full GC造成的。
  • 新项目上线,服务器数量以及配置不足,对于性能的扩展只能靠服务器的增加,而不能通过JVM的调优达到实现服务器性能的突破。
  • 面试经常会问到JVM的一些问题,但是当面试官问到你实际的落地点时,你就会茫然不知所措,没有条理性,或者答非所问。

我会慢慢的在学习种理解上面的这些描述。不过在学习之前,由于可能读者的水平可能会有一些不同,所以本堂课我首先会让你们了解一些基础性的知识。

1.4 编程语言

编程语言(英语:programming language),是用来定义[计算机程序]的[形式语言]。它是一种被[标准化]的交流技巧,用来向[计算机]发出指令。一种能够让[程序员]准确地定义计算机所需要使用数据的计算机语言,并精确地定义在不同情况下所应当采取的行动。

说白了就是让人类能够和计算机沟通,所以要学习计算机能够懂的语言

1.3 计算机[硬件]能够懂的语言

1.3.1 计算机发展史

  • 1946-1958:电子管计算机
  • 1958-1964:晶体管计算机
  • 1964-1970:集成电路计算机
  • 1970-至今:大规模集成电路计算机
  • 猜想:未来以蛋白质分子作为原材料,量子计算机[已经有了]

1.3.2 计算机体系结构

遵循冯诺依曼计算机结构

1.3.3 计算机处理数据过程

(1)提取阶段:由输入设备把原始数据或信息输入给计算机存储器存起来

(2)解码阶段:根据CPU的指令集架构(ISA)定义将数值解译为指令

(3)执行阶段:再由控制器把需要处理或计算的数据调入运算器

(4)最终阶段:由输出设备把最后运算结果输出

本质上就是CPU取数据指令然后返回

CPU=存储器+运算器+控制器

1.3.4 机器语言

我们把CPU能够直接认识的数据指令,称为机器语言,也就是010101001这种形式

1.3.5 不同厂商的CPU

单核、双核、多核

Intel、AMD、IBM等

不同CPU使用的CPU指令集是不一样的,这就会有不兼容的问题

而且要是直接操作01这种形式的,非常麻烦并且容易出错,硬件资源管理起来也不方便

1.3.6 操作系统

  • 向下对接指令系统、管理硬件资源
  • 向上提供给用户简单的操作命令和界面

1.3.7 汇编语言

低级语言,通过汇编器翻译成机器语言

MOV、PUSH、ADD等

对机器友好,执行效率比较高,移植性差

但是人类操作起来还是不太方便,或者需要专业的人员

1.3.8 高级语言

C、C++、Java、Python、Golang等

最终肯定还是要转换成机器能够懂的机器语言

1.3.9 编译型和解释型

1.3.9.1 编译型

使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。

C、C++、GoLang

编译型语言:
执行速度快、效率高;依靠编译器、跨平台性差些。
把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。

1.3.9.2 解释型

**使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。 **

Python、Javascript

解释型语言:
执行速度慢、效率低;依靠解释器、跨平台性好。
把做好的源程序翻译一句,然后执行一句,直至结束。

1.3.9.3 Java呢?

Java属于编译型+解释型的高级语言

其实并不是因为有javac将Java源码编译成class文件,才说Java属于编译+解释语言,因为在这个编译器编译之后,生成的类文件不能直接在对应的平台上运行。
那为何又说Java是编译+解释语言呢?因为class文件最终是通过JVM来翻译才能在对应的平台上运行,而这个翻译大多数时候是解释的过程,但是也会有编译,称之为运行时编译,即JIT(Just In Time)。
综上所述,Java是一门编译型+解释型的高级语言。

1.4 So JVM是什么?

Java Virtual Machine(Java虚拟机)

Write Once Run Anywhere

1.5 JDK JRE JVM

Java官网:Home: Java Platform, Standard Edition (Java SE) 8 Release 8

Reference -> Developer Guides -> 定位到:https://docs.oracle.com/javase/8/docs/index.html

JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8, plus tools such as the compilers and debuggers necessary for developing applets and applications. JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming language. Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.

02 JVM到底该学习什么

或者换句话说,JVM到底从哪边开始学习起?

(1)源码到类文件
(2)类文件到JVM
(3)JVM各种折腾[内部结构、执行方式、垃圾回收、本地调用等]

2.1 源码到类文件

2.1.1 源码demo

package com.example.jvmcase.basic;class Person {private String name = "yzt";private int age;private final double salary = 100;private static String address;private final static String hobby = "Programming";private static Object obj = new Object();public void say() {System.out.println("person say...");}public static int calc(int op1, int op2) {op1 = 3;int result = op1 + op2;Object obj = new Object();return result;}public static void main(String[] args) {calc(1, 2);}
}

编译: javac -g:vars Person.java ---> Person.class

2.1.2 前期编译

**Person.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 **

-> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Person.class文件

2.1.3 类文件(Class文件)

2.1.3.1 16进制

2.1.3.2 The ClassFile Structure

官网: Chapter 4. The class File Format

ClassFile {u4             magic;u2             minor_version;u2             major_version;u2             constant_pool_count;cp_info        constant_pool[constant_pool_count-1];u2             access_flags;u2             this_class;u2             super_class;u2             interfaces_count;u2             interfaces[interfaces_count];u2             fields_count;field_info     fields[fields_count];u2             methods_count;method_info    methods[methods_count];u2             attributes_count;attribute_info attributes[attributes_count];
}

2.1.3.3 Simple analysis

u4 :cafebabe

magic:The magic item supplies the magic number identifying the class file format

u2+u2:0000+0034,34等于10进制的52,表示JDK8

minor_version
major_version

u2:003f=63(10进制)

constant_pool_count:
The value of the constant_pool_count item is equal to the number of entries in the constant_pool table plus one.

表示常量池中的数量是62

cp_info constant_pool[constant_pool_count-1]

The constant_pool is a table of structures representing various string constants, class and interface names, field names, and other constants that are referred to within the ClassFile structure and its substructures. The format of each constant_pool table entry is indicated by its first "tag" byte.
The constant_pool table is indexed from 1 to constant_pool_count - 1.

常量池主要存储两方面内容:字面量(Literal)和符号引用(Symbolic References)

字面量:文本字符串,final修饰等
符号引用:类和接口的全限定名、字段名称和描述符、方法名称和描述符

2.1.3.4 javap验证

JDK自带的命令

javap -h

可以验证一下上述Classfile Structure前面几块内容的正确性

javap -v -p Person.class 进行反编译,查看字节码信息和指令等信息

是否有一种感觉?

JVM相对class文件来说可以理解为是操作系统;class文件相对JVM来说可以理解为是汇编语言或者机器语言。

2.1.3.5 Continous analysis

上面分析到常量池中常量的数量是62,接下来我们来具体分析一下这62个常量

cp_info constant_pool[constant_pool_count-1] 也就是这块包括的信息

cp_info其实就是一个表格的形式

**All **constant_pool table entries have the following general format:

cp_info {u1 tag;u1 info[];
}

官网:Chapter 4. The class File Format

(1)往下数一个u1,即0a->10:代表的是CONSTANT_Methodref,表示这是一个方法引用

CONSTANT_Fieldref_info {u1 tag;u2 class_index;u2 name_and_type_index;
}

往下数u2和u2

u2,即00 0a->10:代表的是class_index,表示该方法所属的类在常量池中的索引
u2,即00 2b->43:代表的是name_and_type_index,表示该方法的名称和类型的索引

#1 = Methodref       #10,#43

(2)往下数u1,即08->8:表示的是CONSTANT_String,表示字符串类型

CONSTANT_String_info {u1 tag;u2 string_index;
}

往下数u2

u2,即00 2c->44:代表的是string_index

#1 = Methodref       #10,#43
#2 = String          #44

(3)往下数u1,即09->9:表示的是CONSTANT_Fieldref,表示字段类型

CONSTANT_Fieldref_info {u1 tag;u2 class_index;u2 name_and_type_index;
}

往下数u2和u2

u2,即00 0d->13:代表的是class_index
u2,即00 2d->45:代表的是name_and_type_index

#1 = Methodref       #10.#43
#2 = String          #44
#3 = Fieldref        #13.#45

2.2 类文件到虚拟机(类加载机制)

类加载机制是指我们将类的字节码文件所包含的数据读入内存,同时我们会生成数据的访问入口的一种

特殊机制。那么我们可以得知,类加载的最终产品是数据访问入口。

这个时候,看到这张图,我们应该有一个问题,那就是我们的字节码加载的方式,也就是我们的字节码文件可以用什么方式进行加载呢?

加载.class文件的方式

  • 从本地系统中直接加载
典型场景:这个我就不废话了
  • 通过网络下载.class文件
典型场景:Web Applet,也就是我们的小程序应用
  • 从zip,jar等归档文件中加载.class文件
典型场景:后续演变为jar、war格式
  • 从专有数据库中提取.class文件
典型场景:JSP应用从专有数据库中提取.class文件,较为少见
  • 将Java源文件动态编译为.class文件,也就是运行时计算而成
典型场景:动态代理技术
  • 从加密文件中获取,
典型场景:典型的防Class文件被反编译的保护措施

好,聊完了这个问题之后,问题接踵而至,我们的类加载的方式已经了解了,那么加载的流程到底是怎

样的呢?

所谓类加载机制就是

虚拟机把Class文件加载到内存
并对数据进行校验,转换解析和初始化
形成可以虚拟机直接使用的Java类型,即java.lang.Class

2.2.1 装载(Load)

查找和导入class文件

(1)通过一个类的全限定名获取定义此类的二进制字节流**(由上可知,我们不一定从字节码文件中获**

得,还有上述很多种方式)

思考:那么这个时候我们是不是需要一个工具,寻找器,来寻找获取我们的二进制字节流。而我们的java中恰好有这么一段代码模块。可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,实现这个动作的代码模块成为“类加载器”。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

获取类的二进制字节流的阶段是我们JAVA程序员最关注的阶段,也是操控性最强的一个阶段。因为这个阶段我 们可以对于我们的类加载器进行操作,比如我们想自定义类加载器进行操作用以完成加载,又或者我们想通过 JAVA Agent来完成我们的字节码增强操作。

在我们的装载阶段完成之后,这个时候在我们的内存当中,我们的运行时数据区的方法区以及堆就已经有数据了。

  • 方法区:类信息,静态变量,常量
  • 堆:代表被加载类的java.lang.Class对象

即时编译之后的热点代码并不在这个阶段进入方法区

2.2.2 链接(Link)

2.2.2.1 验证(Verify)

验证只要是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。

  • 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。这阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的。

举例:

1.是否以16进制cafebaby开头
2.版本号是否正确

  • 元数据验证

对类的元数据信息进行语义校验(其实就是对Java语法校验),保证不存在不符合Java语法规范的元数据信息。

举例:

1.是否有父类

2.是否继承了final类

因为我们的fifinal类是不能被继承的,继承了就会出现问题。

3.一个非抽象类是否实现了所有的抽象方法

如果没有实现,那么这个类也是无效的。

总结:对类的元数据信息进行语义校验(其实就是对Java语法校验),保证不存在不符合Java语法规范的元数据信息。

  • 字节码验证

进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。获取类的二进制字节流的阶段是我们JAVA程序员最关注的阶段,也是操控性最强的一个阶段。因为这个阶段我们可以对于我们的类加载器进行操作,比如我们想自定义类加载器进行操作用以完成加载,又或者我们想通过JAVA Agent来完成我们的字节码增强操作。

举例:

字节码的验证会相对来说较为复杂 。

1.运行检查

2.栈数据类型和操作码操作参数吻合(比如栈空间只有4个字节,但是我们实际需要的远远大于4个字节,那么这个时候这个字节码就是有问题的)

3.跳转指令指向合理的位置

  • 符号引用验证

这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用验证的目的是确保解析动作能正常执行。

举例:

1.常量池中描述类是否存在

2.访问的方法或者字段是否存在且具有足够的权限

注意:但是,我们很多情况下可能认为我们的代码肯定是没问题的,验证的过程完全没必要,那么其实我们可

以添加参数

-Xverify:none 取消验证。

2.2.2.2 准备(Prepare)

为类的静态变量分配内存,并将其初始化为默认值

private final static int a = 1 ; a = 0;

  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
  • 这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
public class Demo1 {private static int i;public static void main(String[] args) {// 正常打印出0,因为静态变量i在准备阶段会有默认值0System.out.println(i);}
}
public class Demo2 {public static void main(String[] args) {// 编译通不过,因为局部变量没有赋值不能被使用int i;System.out.println(i);}
}

进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的。通常情况下,初始值为零值,假设public static int a=1;那么a在准备阶段过后的初始值为0,不为1,这时候只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值为1是在初始化阶段才会执行。

对于一些特殊情况,如果类字段属性表中存在ConstantValue属性,那在准备阶段变量a就会被初始化为ContstantValue属性所指的值。对于这句话,我们又怎么理解呢?

我们可以看一看我们反编译之后的文件,我们就可以发现有这样一个属性。

思考:ConstantValue属性到底是干什么的呢?

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。非static类型的变量的赋值是在实例构造器方法中进行的;static类型变量赋值分两种,在类构造其中赋值,或使用ConstantValue属性赋值。

思考:在实际的程序中,我们什么时候才会用到ContstantValue属性呢?

在实际的程序中,只有同时被fifinal和static修饰的字段才有ConstantValue属性,且限于基本类型和String。编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值,如果该变量没有被fifinal修饰,或者并非基本类型及字符串,则选择在类构造器中进行初始化。

思考:为什么ConstantValue的属性值只限于基本类型和string?

因为从常量池中只能引用到基本类型和String类型的字面量

那么这个时候,我们来举个例子:

假设上面的类变量a被定义为: private static fifinal int a = 1;

编译时Javac将会为a生成ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将value赋值为1。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

2.2.2.3 解析(Resolve)

把类中的符号引用转换为直接引用

符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进
行。

直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般

不会相同,如果有了直接引用,那引用的目标必定存在内存中。

对解析结果进行缓存

同一符号引用进行多次解析请求是很常见的,除invokedynamic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样的,如果 第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常。

inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中。 indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。这里不演示

2.2.3 初始化(Initialize)

初始化阶段是执行类构造器()方法的过程。

或者讲得通俗易懂些

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

在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

按照程序员的逻辑,你必须把静态变量定义在静态代码块的前面。因为两个的执行是会根据代码编写的顺序来决定的,顺序搞错了可能会影响你的业务代码。

JVM初始化步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并链接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

使用

那么这个时候我们去思考一个问题,我们的初始化过程什么时候会被触发执行呢?或者换句话说类初始化时机是什么呢?

主动引用

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用有六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.carl.Test”) )
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类

被动引用

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)。

卸载

在类使用完之后,如果满足下面的情况,类就会被卸载:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。但是一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少数情况下才会被卸载。

2.2.4 类加载器ClassLoader

在装载(Load)阶段,其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。

什么是类加载器?

  • 负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例的代码模块。
  • 类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。

一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的,这里的同名是指全限定名相同。但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。

2.2.4.1 分类

1)Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
2)Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
3)App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
4)Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

为什么我们的类加载器要分层?

1.2版本的JVM中,只有一个类加载器,就是现在的“Bootstrap”类加载器。也就是根类加载器。但是这样会出现一个问题。

假如用户调用他编写的java.lang.String类。理论上该类可以访问和改变java.lang包下其他类的默认访问修饰符的属性和方法的能力。也就是说,我们其他的类使用String时也会调用这个类,因为只有一个类加载器,我无法判定到底加载哪个。因为Java语言本身并没有阻止这种行为,所以会出现问题。

这个时候,我们就想到,可不可以使用不同级别的类加载器来对我们的信任级别做一个区分呢?

比如用三种基础的类加载器做为我们的三种不同的信任级别。最可信的级别是java核心API类。然后是安装的拓展类,最后才是在类路径中的类(属于你本机的类)。

所以,我们三种基础的类加载器由此而生。但是这是我们开发人员的视角。

public class Demo3 {public static void main(String[] args) {// App ClassLoaderSystem.out.println(new Worker().getClass().getClassLoader());// Ext ClassLoaderSystem.out.println(new Worker().getClass().getClassLoader().getParent());// Bootstrap ClassLoaderSystem.out.println(new Worker().getClass().getClassLoader().getParent().getParent());System.out.println(new String().getClass().getClassLoader());}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3a71f4dd
null
null

JVM类加载机制的三种特性

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。

以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class字节码文件生成Class对象由“双亲委派”机制完成。

  • 父类委托,“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

父类委托别名就叫双亲委派机制。

“双亲委派”机制加载Class的具体过程是:

  1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。
  2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。
  3. 依此类推,直到始祖类加载器(引用类加载器)。
  4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
  5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
  6. 依此类推,直到源ClassLoader。
  7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。

“双亲委派”机制只是Java推荐的机制,并不是强制的机制。

我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应

该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。

  • 缓存机制,缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

而这里我们JDK8使用的是直接内存,所以我们会用到直接内存进行缓存。这也就是我们的类变量为什么只会被初始化一次的由来。

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First,在虚拟机内存中查找是否已经加载过此类...类缓存的主要问题所在!!!Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//先让上一层加载器进行加载c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();//调用此类加载器所实现的findClass方法进行加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {//resolveClass方法是当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等resolveClass(c);}return c;}}

java.sql.Driver

打破双亲委派

双亲委派这个模型并不是强制模型,而且会带来一些些的问题。就比如java.sql.Driver这个东西。JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库总不能放JDK目录里吧。

所以java想到了几种办法可以用来打破我们的双亲委派。

SPI(service Provider Interface) :比如Java从1.6搞出了SPI就是为了优雅的解决这类问题——JDK提供接口,供应商提供服务。编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现,岂不是很爽?

Java 在核心类库中定义了许多接口,并且还给出了针对这些接口的调用逻辑,然而并未给出实现。开发者要做的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。比如JDBC中的DriverManager

**OSGI:**比如我们的JAVA程序员更加追求程序的动态性,比如代码热部署,代码热替换。也就是就是机器

不用重启,只要部署上就能用。OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每

一个程序模块都有一个自己的类加载器,当需要更换一个程序模块时,就把程序模块连同类加载器一起

换掉以实现代码的热替换。

自定义类加载器

package com.example.jvmcase.loader;import java.io.*;public class MyClassLoader extends ClassLoader {private String root;protected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {//此方法负责将二进制的字节码转换为Class对象return defineClass(name, classData, 0, classData.length);}}private byte[] loadClassData(String className) {String fileName = root +File.separatorChar + className.replace('.', File.separatorChar) + ".class";try {InputStream ins = new FileInputStream(fileName);ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 1024;byte[] buffer = new byte[bufferSize];int length = 0;while ((length = ins.read(buffer)) != -1) {baos.write(buffer, 0, length);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public String getRoot() {return root;}public void setRoot(String root) {this.root = root;}public static void main(String[] args) {MyClassLoader classLoader = new MyClassLoader();
//        classLoader.setRoot("D:\\codes\\jvm-case\\src\\main\\java");classLoader.setRoot("D:\\");Class<?> testClass = null;try {
//            testClass = classLoader.loadClass("com.example.jvmcase.basic.Test");testClass = classLoader.loadClass("Test");System.out.println(testClass);Object object = testClass.newInstance();System.out.println(object.getClass().getClassLoader());} catch (ClassNotFoundException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

  • 1、这里传递的文件名需要是类的全限定性名称,即 Test 格式的,因为 defifineClass 方法是按这种格式进行处理的。

如果没有全限定名,那么我们需要做的事情就是将类的全路径加载进去,而我们的setRoot就是前缀地址 setRoot + loadClass的路径就是文件的绝对路径

  • 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
  • 3、这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。

如果我们把Test放在类路径之下,那么我们将会通过AppClassLoader加载

打印结果:

class com.example.jvmcase.basic.Test

sun.misc.Launcher$AppClassLoader@18b4aac2

1、JVM之走进类加载相关推荐

  1. Java虚拟机JVM学习05 类加载器的父委托机制

    Java虚拟机JVM学习05 类加载器的父委托机制 类加载器 类加载器用来把类加载到Java虚拟机中. 类加载器的类型 有两种类型的类加载器: 1.JVM自带的加载器: 根类加载器(Bootstrap ...

  2. 【随笔】JVM核心:JVM运行和类加载

    前言 本篇博客将写一点关于JVM的东西,涉及JVM运行时数据区.类加载的过程.类加载器.ClassLoader.双亲委派机制.自定义类加载器等,这些都是博主自己的一点理解,如果有误,欢迎大家评论拍砖~ ...

  3. 【深入理解JVM】:类加载器与双亲委派模型

    转载自  [深入理解JVM]:类加载器与双亲委派模型 类加载器 加载类的开放性 类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因.在类加载的第一阶段" ...

  4. jvm运行时类加载机制_JVM体系结构:JVM类加载器和运行时数据区

    jvm运行时类加载机制 各位读者好! 在JVM系列的上一篇文章中,开发人员了解了Java虚拟机(JVM)及其体系结构. 本教程将帮助开发人员正确回答以下主题的问题: ClassLoader子系统 运行 ...

  5. 第六篇 JVM核心机制之JVM运行和类加载全过程(一)

    JVM核心之JVM运行和类加载全过程 为什么研究类加载全过程? 有助于连接JVM运行过程 更深入了解java动态性(解热部署,动态加载),提高程序的灵活性 类加载机制 JVM把class文件加载到内存 ...

  6. JVM——深入理解类加载器

    类加载器相关内容有很多,大概分以下几个关键点进行学习, 参考链接 https://www.bilibili.com/video/av47756459 目录 概览 JVM与程序的生命周期 类加载器生命周 ...

  7. 【Android 插件化】插件化原理 ( JVM 内存数据 | 类加载流程 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

  8. JVM基础 - JAVA类加载机制

    JVM基础 - JAVA类加载机制 类的生命周期 其中类加载的过程包括了 加载 . 验证 . 准备 . 解析 . 初始化 五个阶段.在这五个阶段中, 加载 . 验证 . 准备 和 初始化 这四个阶段发 ...

  9. JVM原理系列--类加载过程(有实例)

    原文网址:JVM原理系列--类加载过程(有实例)_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java的类加载流程,有实例. 类加载过程 流程概述 加载=> 链接(验证+准备+解析)=> ...

最新文章

  1. Selenium3+python自动化009- 多选框
  2. eclipse常用快捷键汇总
  3. sed文本处理常见用法
  4. Codeforces Round #664 (Div. 2)
  5. lambda表达式创建一条最简单的线程
  6. boot lvm 分区_LVM磁盘逻辑卷管理
  7. 使用NuGet安装EntityFramework4.2
  8. 团队作业个人博客07
  9. 短期通过PMP考试?
  10. 步进电机工作原理 驱动 _28BYJ-48 以及程序实例下载
  11. 变分法与最优控制matlab,1987_11变分法与最优控制_10654423.pdf
  12. 占位智能家居市场,施耐德电气仅靠一个Wiser系统?
  13. gradient设置上下渐变_CSS3 经典教程系列:CSS3 线性渐变(linear-gradient)
  14. setsockopt()改善socket网络程序的健壮性
  15. 西门子PLC封装TCP通讯块和调试助手进行TCP仿真测试
  16. 漫步者蓝牙自动断开_情人节礼物 就连宋轶都推荐的高颜值真无线蓝牙耳机
  17. Oracle数据库初学者:sql语句创建数据库表空间,建表语句
  18. SQL Server 学习笔记——T-SQL
  19. HTML+CSS项目实践四:给html网页标题栏添加logo图标(ico格式图片)
  20. 剑13 TEST2 听力复盘

热门文章

  1. BaseQuickAdapter设置条目点击事件无效
  2. 2d游戏地图制作html5,如何通过 Cocos2d-html5 使用砖块地图编辑器
  3. Python 打造微信群聊天机器人(带操作界面)
  4. 如何对固定资产、耗材全流程管理
  5. VBA编程常用语句300句
  6. linux系统编程3—文件存储函数
  7. 如何为py图表添加坐标轴/标题/图例等辅助元素
  8. ChatGPT 账号被封?推荐 5 款国外网站,完美替代!
  9. Proteus的安装和介绍及51单片机电路仿真
  10. java基础代码,适合0基础学习者