JVM学习总结
一、JVM基础入门
1.1JVM基础
1.1.1 什么是jdk?
java的开发工具包。
1.1.2 什么是jre
运行时的环境。
1.1.3 什么是jvm?
java技术的核心,java虚拟机,所有的java程序都运行在java虚拟机内部。
特点:
- 一次编译,到处运行。
- 自动内存管理
- 自动垃圾回收功能
jvm的位置
1.1.4 JVM的整体结构
将源代码.java文件 通过javac的指令生成class文件
通过java指令,将字节码文件.class进行解析,首先class文件先通过classloader加载到内存中,会用到一些java相关类库,比如说object或者String等等。进行调用字节码的解释器JIT即使编译器,来对字节码文件进行编译,编译之后,在由执行引擎执行,执行引擎面对的是操作系统和硬件。我们可以把整个的java指令的这一部分流程称为jvm.
1.1.5 Java代码执行流程
java源码在java编译器中进行一系列编译,这个过程是编译原理中代码生成的过程,之后生成字节码文件,再交给Java虚拟机进行类加载和初始化,之后再解析,编译执行,交付给操作系统进行读取识别。
面试题:java语言是解析,还是编译执行?
解析和编译是混合的,针对常用的代码,会把代码做成一种即时编译的,支持本地的,那么在下次使用的时候就不需要解释器对代码进行一句句的解析来执行,执行引擎可以将代码直接交给操作系统。让其进行调用,效率能够提高,当然并不是所有的代码都要被JIT进行即时编译。原因是java本身需要跨平台。
1.1.6 JVM的生命周期
- 虚拟机的启动
Java虚拟机的启动是通过引导类加载器(bootsrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
- 虚拟机的执行
他的任务就是执行java程序。程序开始运行时jvm就开始运行,程序结束时jbm就停止。执行一个java程序的时候,实质上执行的是一个叫做java虚拟机的进程。
- 虚拟机的退出
有以下几种情况:
(1)程序正常结束
(2)程序在执行过程中遇到异常或者错误而异常终止
(3)操作系统出现错误导致的jvm进程终止
(4)某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作。
(5)除此之外,JNI规范描述了用JNI Inovacation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。
1.2 从跨平台的语言到跨语言的平台
JVM其实是一个跨语言的平台,java是一个跨平台的语言。
除了java语言jvm还支持多门语言,将近100多种,JVM也是提供了一些规范。他也帮我们屏蔽了一些操作系统,可以在多种系统上进行操作。
JVM怎么决定其他语言也可以在JVM上跑,因为class。
其他语言只要能够生出class文件的那么都可以在JVM上执行。
java程序,可以在各种平台上执行的,并且不用修改东西,以前的c语言和c++都是做不到的。
如果是程序内部编译成的类Class文件的二进制流 也是可以用jvm来执行的,所以我们说java和JVM 是没有任何关系的。
JVM是有一种规范的 定义了java虚拟机能够执行什么东西
规范在oracle官网
https://docs.oracle.com/javase/specs/index.html
1.3 常见的虚拟机
-
Hotspot oracle官方的,我们做实验用的JVM –java –version
在命令行窗口输入java - version 可以查看jdk,虚拟机的版本
-
Jrockit -BEA 曾号称世界上最快的JVM 被oracle收购了合并hotspot,专注于服务端应用,Jrockit内部不包含解析器实现。
-
TaoBaoVM -hotspot深度定制版
淘宝定制款的JVM,基于OpenJDK开发了自己的定制版本AlibabaJDK
-
LiquidVM 直接针对硬件
它是没有操作系统,没有Linux和unix 运行效率比较高。可以说它自己实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等。
-
azul zing 最新垃圾回收的业界标杆 土豪版本 银行电信
它的垃圾回收号称1毫秒以内的
-
J9 – IBM,广泛应用于IBM的各种java产品
-
Microsoft VM
一般大的厂商不会依赖oracle的东西 IBM 微软都有自己的平台,因为版权问题,所以不用研发的自己的JVM ,java中的收费是针对虚拟机来说的。
二、Class文件结构
2.1 Class file format
先编写一个简单的代码,使用IDEA,这个简单的例子是用来对后面class文件结构进行分析的案例。
通过编译能够生成class文件
我们可以通过一些插件来分析二进制字节码文件,比如sublime_text,或者idea中的插件Bined,我们可以看到生成的字节码文件是以十六进制的数据流展现的,我们可以把它转换成二进制等其他形式。
通过上述内容我们可以得出,Class文件是一组以8个字节为基础单位的二进制字节流。各项数据会严格的按照顺序紧凑的排列在class文件中,中间没有分隔符,使得class文件存储的内容几乎全部都是程序运行的。
2.1.1 插件安装与使用
1.安装
IDEA 插件 在file下选择setting 然后在Plugins 输入Bined对该插件进行安装,下载完毕之后点击apply。
2.使用
安装好了以后,鼠标点击file 选择openAsBinary,选择.calss文件打开
2.2 Classfile文件结构解析
Java虚拟机规范规定的,class文件格式采用的类似C语言的结构体的伪结构来存储的,这种结构只有两种数据类型。 无符号数 和 表
无符号数:
属于基本数据类型 主要用于描述数字 索引符号 数量值 或者按照UTF-8编码构成的字符串值
数据类型 U1 U2 U4 U8 也只是逻辑上的区分。
u1 —表示一个字节
u2 —表示两个字节
U4 — 依次类推
u8 —依次类推
表: 由多个无符号数或者其他表作为数据项构成的复合数据类型。所有的表都习惯以_info结尾 表主要用于描述有层次关系的复合结构数据。 比如 方法、字段 需要注意的是class文件没有分隔符,所以每个二进制数据类型都是严格定义的 具体的顺序如下:
2.2.1 魔数Magic Number
-
每一个class文件的头4个字节被称为魔数magicNumber
-
唯一的作用:是用于确定这个文件是否为一个能被虚拟机接受的calss文件
-
Class文件魔数值为0xCAFEBABE,不会改变。如果一个文件不是以CAFEBABE开头,那么他就肯定不是java的class文件。
为什么要使用魔数来识别?
很多的文件存储标准中都使用魔数来识别文件的身份。譬如图片格式.gif或者jpeg等在文件的头部都存在魔数,使用魔数而不是文件的扩展名称来判断,这种情况是处于安全的考虑。
2.2.2 class文件版本号
紧挨着魔数的4个字节存储的是Class的文件的版本号
版本号:
1.次版本号 --minor_version第5、6个字节用于表示次版本号
例如:0x00 00
关于次版本号,曾经在现代Java(即Java 2) 出现前被短暂用过,JDK 1.0.2支持的版本45.0~ 45.3(包括45.0~45.3)。JDK 1.1支持版本45.0~ 45.65535,从JDK 1.2以后, 直到JDK 12之前次版本号均为被使用,全部固定为零。
如果Class文件中֯用了该版本JDK 尚未列入正式特性清单中的预览功能,则必须把次版本号标识为65535,以便Java虚拟机在加载类文件时能够区分出来。
2.主版本号 --major_version 第7、8个字节用于表示主版本号
例如:0x00 34
这个版本号3随着jdk版本的不同而表示不同版本的范围。java的版本是从45开始的。如果class的版本号超过虚拟机的版本 会被拒绝执行。
**例如:0x00 34 十进制为52 表示用的是JDK1.8,该版本号说明这个是可以被JDK8版本及以上的虚拟机执行的Class文件,不能被JDK8版本以下的虚拟机执行。
**
例如:JDK1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件, 而JDK 1.2则能支持45.0~46.65535的Class 文件。
JDK1.2 – 0x002E 46
JDK1.3 – 0x002F 47
JDK1.4 – 0x0030 48
JDK1.5 – 0x0031 49
JDK6 – 0x0032 50
JDK7 – 0x0033 51
JDK8 – 0x0034 52
2.2.3 常量池
CONSTANT_POOL_COUNT和CONSTANT_POOL
紧跟着魔数与版本号之后的是常量池入口,常量池简单理解为class文件的资源库。
特点:
-
它是class文件结构中与其他项目关联最多的数据类型
-
是占用class文件空间最大的数据项目之一
-
是在文件中第一个出现的表类型数据项目。
常量池的数量是不固定,所以在常量池的入口需要放置一个u2类型的数据,代表常量池的计数值CONSTANT_POOL_COUNT。
CONSTANT_POOL_COUNT 从1开始计数的。 class文件结构中只有常量池的容量计数是从1开始的。第0项腾出来满足后面某些指向常量池的索引值的数据,在特定的情况下需要表达“不引用任何一个常量池项目” 把索引值的第0项留给JVM自己用。
CONSTANT_POOL是没有索引值为0的入口的,但是在CONSTANT_POOL_COUNT缺失的第0项也是要被计算在内的。
比如CONSTANT_POOL 中有14项 那么CONSTANT_POOL_COUNT的数值就是15
常量池中主要存放两大类常量:
-
字面量: 比较接近java语言层面的常量的概念 比如 字符串 被final关键字声明的常量值。
-
符号引用: 属于编译原理方面的概念 包括三项。
- 类和接口的全名
- 字段的名称和描述符
- 方法的名称和描述符
在加载class文件的时候 是进行动态连接的。在class文件中不会保存各个方法和字段的最终内存布局信息。(需要经过转换) 当虚拟机运行时 需要从常量池获得对应的符号引用,再在类创建时或者运行时解析并翻译到具体的内存地址中。
CONSTANT_POOL_COUNT 占2个字节 本例中为0x20 转换成十进制为32 说明常量池中有31个常量 ----从1开始计数 其他集合类型均从0开始。 索引值为1-31 第0项常量具有特殊意义。
CONSTANT_POOL 表示的是类型数据集合,在该常量池中,每一项常量都是一个表 共有14种 -----JDK1.7版本,这14种结构的表都是不相同的结构数据。14个表都有一共同的特点,都是由u1的标志位开始的,可以通过这个标志位来判断这个常量属于哪种常量的类型。
2.2.4 access_flag
在常量池结束之后,紧接着的2个字节代表访问标志。 用于表示对该类或接口的访问权限以及该类或接口的属性。
- access_flags中一共有16个标志位可以用, 当前只定义了其中9个,没有用到的标志位要求一律为零。
- 访问标志由标志值相与运算得到。
2.2.5 this_class
当前类的名称。
- 该this_class 项目的值 必须是constant_pool表中的有效索引,该constant_pool索引处的条目必须是表示此文件定义的类或接口 CONSTANT_Class_info 结构class。
- Class文件中由类索引(this_class)、父类索引(super_class)和接口索引(interfaces)这三项数据来确定该类型的继承关系。
- 类索引用于确定这个类的全限定名。
- 类索引、父类索引、接口索引集合,按顺序排列在访问标志之后
2.2.6 super_class
父类索引用于确定这个类的父类的全限定名。
-
必须是constant_pool表中的有效索引, 如果super_class的值不为0 则constant_pool中的条目必须为CONSTANT_Class_info 结构 这个结构表示此类的文件定义的类的直接超类。直接超类不能在其classfile结构的access_flag项中设置 ACC_FINAL 标志。
其实要描述的意思就是说 如果superclass指代的超类,那么它就不能被final修饰。
-
由于Java语言不允许重继承,所以父类索引只有一个,除了 java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类都不为0。
-
类索引、父类索引、接口索引集合,按顺序排列在访问标志之后
2.2.7 ByteCode插件安装
- javap指令
- JBE 插件可以直接修改classfile
- JClasslib IDEA插件之一
javap -v 字节码文件路径
从上图可以看出,对文件进行了MD5的加密
Minor Version ----0
Major Version ----52 jdk1.8
Acess_flag ---- public super
#4 = Class ---- #24 this_class 编号为多少
#5 = Class ----#25 super_class 编号为多少
- JBE
工具 不仅可以查看bytecode也可以进行修改,可以通过汇编语言编写。
- JClasslib
setting 里的plugins里面下载插件jclass
如果字节码没有生成 可以build project
如何使用?选中类名,点击view,里面的show ByteCode with JClass
访问标志为什么是0x0021 这是一个按位与的运算,通过ACC_PUBLIC&ACC_SUPER这两个属性的按位与运算得来的值。使用2个字节来表示,可以代表很多内容。
2.2.8 常量池详细解析
总共有18个编号的常量类型。
- 编号1:CONSTANT_UTF8_INFO
有一个标志位
TAG1 -----占用一个空间字节
length: utf-8字符串占用的字节数
Bytes 长度为length字符串,用于表示utf-8的编码的字符串
-
没有编号2
-
编号3:CONSTANT_integer_info
Tag3
Bytes 4个字节 Big_Endian(高位在前)存储int类型的值
- 编号4:CONSTANT_float_info
Tag4
Bytes 4字节Big_Endian(高位在前)存储float类型的值
- 编号5:CONSTANT_long_info
Tag5
Bytes 8字节Big_Endian(高位在前)存储long类型的值
- 编号6:CONSTANT_double_info
Tag6
Bytes 8字节Big_Endian(高位在前)存储double类型的值
- 编号7:CONSTANT_Class_info
Tag7
index 2个字节 指向类的全限定名的项的索引
类和接口符号引用
- 编号8:CONSTANT_String_info
Tag8
index 2个字节 指向字符串的字面量的索引
- 编号9:CONSTANT_Field_info
Tag9
index 2个字节 指向声明字段的类或接口的描述符CONSTANT_Class_info索引项
index 2个字节 指向字段描述符CONSTANT_NameAndType的索引项
- 编号10:CONSTANT_Methodref_info
Tag10
index 2个字节 指向声明字段的类或接口的描述符CONSTANT_Class_info索引项
index 2个字节 指向字段描述符CONSTANT_NameAndType的索引项
类中方法的符号引用
- 编号11:CONSTANT_InterfaceMethodref_info
Tag11
Index 2个字节 指向声明字段的类或接口的描述符 CONSTANT_Class_info的索引项
Index 2个字节 指向字段描述符CONSTANT_NameAndType的索引项
接口中方法的符号引用
- 编号12 :CONSTANT_NameAndType
Tag12
index 2个字节 指向该字段或方法名称常量项的索引
index 2个字节指向该字段或方法描述符常量项的索引
字段或方法的符号引用
- 编号15: CONSTANT_MethodHandler_info
Tag15
Reference_kind 1个字节 1-9之间的一个值 决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
Reference_index 2个字节 对常量池的有效索引。
表示方法句柄
- 编号16: CONSTANT_MethodType_info
Tag16
Descriptor_index 2个字节 指向UTF8_info 结构表示的方法描述符
-
编号18:CONSTANT_InvokeDynamic_info
Tag18
Bootstrap_method_attr_index: 2个字节 当前class文件中引导方法表的bootstrap_methods[] 数组的有效索引
Name_and_type_index: 2个字节 指向NameAndType_info 表示方法名和方法描述符。
表示动态方法的调用点。
2.2.8.1 案例分析
Method属性
Method属性包含三个字段值
名称access_flag 类型u2 数量1个attributes_count 1
名称 name_index 类型u2 数量1个 attribute_info attributes ----attributes_count
名称 descriptior_index 类型u2 数量1个
descriptior_index
-
参数列表(参数类型) 后-返回值
-
void m() 等同于 ()V
-
String toString() ->()Ljava/lang/String;
-
Long pos(int[] arr1,int arr2,long length) ->([IIJ)J
[ 一维数组
[[ 表示二维数组
2.2.9 interfaces 和Fields选项 method选项
1.interfaces
- 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字 (如果这个Class文件表示的是一个接口,则应当是 extends关键字) 后的接口顺序从左到右排列在接口索引集合中。
- 类索引、父类索引、接口索引集合,按顺序排列在访问标志之后
- 对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
2.2.10 attributes_count 和attribute
附加属性 方法中的附加属性就是code,那么code在这里是比较重要的概念,code是具体代码的实现,当我们写入方法的时候,它能够把方法中代码转化为一条条指令。
在官方文档中,我们可以看到很多十六进制的数字,那么它们对应方法中code的内容实现。
也可以鼠标右键点击
Attributes附加属性
附加属性中 有的代码中存在内容,有的不存在内容
-
既有预定义的属性,也可以自定义 java虚拟机会自动忽略它不认识属性
-
Code 表示的是方法表 方法表能够编译成字节码指令,还存放了操作数栈和局部变量的信息。
u2 attribute_name_index 指向常量池中的CONSTANT_UTF8_info 存放的当前属性的名字就是code。
u4 attribute_length 表示的code属性的长度 (不包括前6个字节)。
u2 max_stack 指定当前方法被执行引擎执行的时候,在栈帧中需要分配的操作数栈的大小
u2 max_locals 指定当前方法被执行引擎执行的时候,在栈帧中需要分配的局部变量表的大小
u4 code_length 指定方法字节码的长度, class文件中每条字节码都占用一个字节
u1 code 存放字节码指令本身,它的长度是code_length个字节。
U2 exception_table_length指定异常表的大小
Exception_table异常表 作用对try-catch-finally的描述,可以把它看成是一个数组。每一个数组项都是一个exception_info结构, 一般来说每个catch块对应一个exception_info,编译器也可能会对当前的方法生成一些exception_info.
U2 start_pc 是从字节码code属性中的一部分 起始处到当前异常处理器的起始处的偏移量量
u2 end_pc 从字节码起始处到当前异常处理器 末尾的偏移量
u2 handler_pc 是指当前异常处理器用于处理异常(即catch块)的第一条指令相对于字节码开始处的偏移量。
U2 catch_type 是常量池的索引 指向的是常量池CONSTANT-Class_info 数据项,描述了catch块中的异常类型的信息。这个类必须是java.lang.Throwable的或者是它的子类。
总结:
如果偏移量从start_pc到end_pc之间,如果字节码出现了catch_type所描述的异常,那么就跳转偏移量到handler_pc的字节码中去执行。如果catch_type 为0 就代表不引用任何常量池的信息,那么这个exception_info 用于实现finally的子句。
U2 attribute_count 表示的是code属性中存在的其他属性的个数。会出现在 class中的属性,在field属性也有,在method属性也有
Attributes 可以把它看成是个数组,里面存放了code属性的其他属性。
ConstantValue ----字段表 final关键字自定义的常量值
Deprecated —类 方法表 字段表
Exception 异常表
EnclosingMethod 类文件 局部类或匿名类的外部封装方法
InnerClass 类文件 内部类列表
可选属性
LineNumberTable 源码的行号和字节码行号的对应关系 可以把这个属性看成是一个数组,
数组中的每项LineNumberinfo结构描述了一条字节码和源码行号的对应关系
LocalVariableTable 建立了方法中的局部变量与源代码中的局部变量的对应关系。
三、类加载和初始化
面试题:
1.描述一下类加载器的层次?
引导类加载器(bootstrap class loader)
用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar,或sun.boot.class.path路径下的内容),是用原生代码来实现的(C实现的),并不继承自java.lang.ClassLoader。
加载扩展类和应用程序类加载器,并指定它们的父类加载器。
扩展类加载器(extensions class loader)
用来加载java的扩展库(JAVA_HOME/jre/lib/ext/*.jar,或java.ext.dirs路径下的内容)java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类。
有sun.miscLauncher$ExtClassLoader实现,继承自java.lang.ClassLoader
应用程序类加载器(application class loader)
它根据java应用的类路径(classpath,java.class.path路径)来加载指定路径的类,一般来说,java应用的类都是由它来完成加载的
由sun.misc.Launcher$AppClassLoader实现,继承自java.lang.ClassLoader
自定义类加载器
开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
说明:在java中由于类的加载采用的是双亲委托机制,上面几种类加载器是父子关系,其中引导类加载器为基础。
2.什么叫双亲委派?
3.为什么要双亲委派?
Class文件 如何加载到内存中的
3.1* Class Cycle (重点)
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程称为虚拟机的类加载机制。
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,他的整个生命周期将会经历==加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)==七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:他在某些情况下可以在初始化阶段之后再开始,是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
这些阶段通常是互相交叉地混合进行的。
Loading加载、Linking连接、Initialization初始化
Class文件在我们硬盘中,那么他是如何加载到内存中的,总共需要三个大的步骤
1.Loading步骤
Loading是将本地的classfile(字节码文件)的二进制的内容加载到内存中。
2.Linking步骤
- Verification校验
Verification的主要过程是用来校验,如果加载的文件的字节码头不是CAFEBABE,(判断一下这个字节码文件是不是java的字节码文件)该过程就会被拒绝。
- Preparation准备(面试)
Preparation过程:主要作用就是将静态变量赋默认值。
假设定义public static int i=8;在这个过程中并不是把i的值赋值成8,而是要对静态变量i进行默认值的赋值 也就是0;
- Resolution解析
该过程将class文件中常量池用到的一些符号 引用转换为内存地址
3.Initializing 初始化
给静态变量进行赋值为初始值,才会调用静态代码块。
3.2 ClassLoader
JVM本身有个类加载器的层次 这个类加载器就是普通的Class,这个加载器的层次就是用来加载不同的class 。
注意: 任何一个classfile被加载到内存中的都会存在两个部分,
第一个部分 二进制的classfile确实被load内存中
第二个部分 生成的class类的对象,class中还会存在其他对象(String ,Object),引用到class对象,而class类对象指向classfile的内存加载
扩展: Class对象究竟存储在哪里?
Class对象存储在metaspace里面
Metaspace 是JDK1.8版本出现的,Metaspace 就是方法区methodarea 1.8版本移出了永久代,原本在1.8版本之前 PermGenerationspace部分变更成了metaspace 而这两个地方指代的都是方法区。
- 最顶层:
BootstrapClassLoader
加载lib/rt.jar charset.jar等核心类 C++实现。
主要负责加载jdk中最核心的jar ,例如runtime.jar 或者是我们平时锁说的String.class,Object.class 都是位于lib/rt.jar
会出现null值 调用的是最顶层加载器,在java的类中没有这样的对象去应对他。
- 第二层:
ExtensionClassLoader
记载扩展的jar包,,jre/lib/ext/*.jar
或由-Djava.ext.dirs指定
- 第三层
AppClassLoader
加载classpath指定的内容
- 第四层
自定义加载起
加载自定义类的内容。
怎么才能够知道哪些类是由哪些加载器进行加载的呢?
package edu.yau;
import sun.net.spi.nameservice.dns.DNSNameService;
public class ClassLoaderTest01 {
public static void main(String[] args) {
//由最顶层的加载器加载的,核心代码库,由c++编写,java中没有一个可以对应的对象
System.out.println(String.class.getClassLoader());
System.out.println(sun.awt.HKSCS.class.getClassLoader());
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
//当前本类对象获取的类的加载器
System.out.println(ClassLoaderTest01.class.getClassLoader());
//由最顶层的加载器加载的
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());
System.out.println(ClassLoaderTest01.class.getClassLoader().getClass().getClassLoader());
}
}
类的加载器的加载过程叫做双亲委派。
在双亲委派的过程中存在一个概念叫父加载器。
这里的父加载器不是继承关系。
这个图描述的是语法上的一种继承关系,而继承关系和父加载器没有关系。
父加载器指代的是ClassLoader源码中有一个变量叫ClassLoader类型 名称为parent。
3.3* 双亲委派 (面试)
Class文件通过自定义的classloader进行加载,如果他没有加载,那么则委托它的父加载器appclassloader 加载, appclassloader 判断是否为本地加载 如果有则直接加载,如果没有则继续向上委托,直到顶层的加载器bootstrapClassLoader,但是当顶层的加载器,也没有加载,就会向下委托,当所有的下级加载器都没有加载那么则抛出异常 classNotFound 异常,如果下级加载器能够加载,那么就由下级加载器进行加载。
双亲:指的有一个从子到父的过程 又有一从父到子的过程
委派:自己不想做的事情 委托别人去完成
向上委派的时候 父加载器都是到 Cache中取寻找
可以把这个缓存理解成是一个list或者是一个数组。
面试题 为什么要去使用双亲委派?
-
防止加载同一个class文件,保证数据的安全
-
保证核心的class文件不被篡改,即使被篡改了也不会加载,即使被加载也不会是同一个class对象 为了保证class的执行安全。
这部分代码是被写死的。
3.4 父加载器
父加载器不是类的加载器的加载器,也不是加载器的父类的加载器。
父加载器其实指代的是 ClassLoader源码中 有一个变量 这个变量叫Classloader类型 名称叫parent。
package edu.yau;
public class ClassLoaderTest02 {
public static void main(String[] args) {
//获取本类的加载器
System.out.println(ClassLoaderTest02.class.getClassLoader());
//获取本类的加载器的class对象的加载器--顶级加载器加载的
System.out.println(ClassLoaderTest02.class.getClassLoader().getClass().getClassLoader());
//获取本类加载器的父类加载器
System.out.println(ClassLoaderTest02.class.getClassLoader().getParent());
System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent());
//System.out.println(ClassLoaderTest02.class.getClassLoader().getParent().getParent().getParent());
}
}
3.5 类加载器的范围
从上个案例的执行结果中,我们可以看出appclassloader和extclassloader 都是Launcher的内部类。 Launcher是classloader的包装类启动类
在launcher源码中
//sun.boot.class.path 是BootstrapClassloader的加载路径
private static String bootClassPath = System.getProperty("sun.boot.class.path");
//java.class.path 是AppClassloader的加载路径
final String var1 = System.getProperty("java.class.path");
//java.ext.dirs 是ExtClassLoader的加载路径
String var0 = System.getProperty("java.ext.dirs");
3.6 自定义加载器
package edu.yau;
public class ClassLoaderTest04 {
public static void main(String[] args) throws ClassNotFoundException {
Class cla = ClassLoaderTest04.class.getClassLoader().loadClass("edu.yau.ClassLoaderTest04");
System.out.println(cla);
}
}
Tomcat 加载的Servlet
Spring框架中加载ApplicationContext
比如在写一些类库的时候或者修改底层框架时。想加载哪个类就可以加载谁
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 加锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//继续使用parent的classloader 递归调用loadClass方法
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
}
// 调用findClass方法去找class
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
-
继承ClassLoader
-
重写模板方法 findClass
----调用defineClass方法
从目录中读取class文件,将class文件通过自定义加载器进行加载
利用IO流
Java语言是比较容易被反编译
-
防止反编译
-
防止篡改
-
可以给class文件进行加密 解密
3.7 编辑器
解释器: bytecode-interpreter
JIT 即时编辑器 Just In Time compiler
Java语言究竟是一个解释型语言还是编译式的语言
想解释器的可以用解释器,想编译也可以用编译器 看需求是怎么写的 可以通过JVM的一些参数进行设置。
默认的情况是一种混合模式
混合模式:使用解释器+热点编辑器 hotspot
起始阶段采用解释来执行
热点代码的检测 默认值为10000
多次被调用的方法(方法计数器:检测方法的执行频率)
多次被调用的循环(循环的计数器:检测循环的执行频率)
当这样的一个循环或者是一个方法,或者是一段代码,一直都会被多次调用的时候,也就是这段代码执行频率特别高的情况下,那么干脆直接将这段代码编译成本地的代码,在下次直接访问的时候,直接访问本地的代码就可以。就不需要解释器对其进行解释执行。从而达到效率的提升。这种执行代码的方式被称为混合模式。
那么为什么不直接编译成本地代码,编译的执行速度更快?能够提高效率?
-
现在的解释器的执行效率已经是非常高的了,在一些简单的代码执行上,它并不属于编译器。
-
如果要执行的程序 依赖的类库特别多的情况下,在虚拟机中编译一遍,那么启动的过程会非常的缓慢。
-Xmixed 为混合模式:
开始解释执行,启动速度比较快,对热点代码进行检测和编译。
-Xint 解释模式
启动速度很快,执行较慢
-Xcomp 纯编译模式,
启动较慢,执行较快
测试这三个jvm参数
默认的混合模式
在JVM的执行参数中 -Xint 解释模式
纯编译的模式-Xcomp
3.8 懒加载
严格来讲应该叫lazyInitializing
JVM规范中并没有规定什么时候加载
严格的规定了初始化的规则 扩展
- New对象 getstatic 访问静态变量时 putstatic 访问静态实例时,invokestatic指令
以上指令是必须要初始化这个类 访问final变量除外。
-
当反射调用的时候
-
初始化子类的时候 首先父类初始化
-
虚拟机启动时 被执行的主类必须要初始化
-
动态语言支持java.lang.invoke.MethodHandler解析结果为REF-getstatic REF-putstatic REF-invokestatic的方法句柄时 该类必须要初始化。
面试题:如何打破classloader的双亲委派模式?
去重写Classloader中的loadClass方法 而不是findClass方法 这个时候就能够打破双亲委派的机制。
什么时候需要打破需要去打破双亲委派的机制:
-
在JDK1.2版本之前 要自定义classloader的话 必须要重写loadClass方法
-
在一个线程中设定自己的线程的上下文的加载器对象 ThreadContextClassLoader 可以实现基础调用实现类的代码, 通过thread.setContextClassLoader 来设定。
-
模块化的 热部署 热启动
像osgi和tomcat 都有自己的模块指定classloader,可以加载同一个类库的不同版本的对象,目前这个方式用的比较多。
Tomcat中 Webapplication 对象是可以存在多个的,有两个Webapplication 被加载,但是他们的版本不同,这种情况下可以打破双亲委派的。
注意:类的名字的是相同的 只是说版本不同 如果采用双亲委派的机制,那么这两个对象是不可能加载到同一个空间里面 因为加载的过程中,发现在同一空间有同名的类,那么他一定不会被加载。
所以tomcat的每一个Webapplication 都有一个classloader。