*******************JVM笔记(一)*********************
JVM学习笔记 尚硅谷JVM视频
JAVA与JVM结构体系
1. JAVA与JVM简介
-
java: 跨平台的语言
-
JVM: 跨语言的平台
- 随着Java7的正式发布,Java虚拟机的设计者们通过JSR-292规范基本实现在Java虚拟机平台上运行非Java语言编写的程序。
- Java虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的,它只关心"字节码"文件,也就是说java虚拟机拥有语言无关性,并不会单纯地与Java语言"终身绑定",只要其他编程语言的编译结果满足并包含Java虚拟机的内部指令集,符号表以及其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行。
- 字节码
- 我们平时说的Java字节码,指的是用java语言编译成的字节码,准确的说任何能在jvm平台上执行的字节码格式都是一样的,所以应该统称为:jvm字节码。
- 不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的jvm上运行。
- java虚拟机与java语言并没有必然的联系,它只与特定的二进制文件格式—Class文件格式所关联,Class文件中包含了java虚拟机指令集(或者称为字节码,Bytecodes)和符号表,还有一些其他辅助信息。
- 多语言混合编程
- java平台上的多语言混合编程正成为主流,通过特定领域的语言取解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向.
- 试想一下,在一个项目之中,并行处理用Clojure语言编写,展示层使用过JRuby/Rails,中间层用过java,每个应用层都将使用不用的编程语言来完成,而且,接口对每一层的开发者都是透明的。各种语言之间的交互不存在任何困难,就像使用自己语言的原生API一样方便,因为它们最终都运行在一个虚拟机上。
- 对这些运行与Java虚拟机上,Java之外的语言,来自系统级的,底层的支持正在迅速增强,以JSR-292为核心的一系列项目和功能改进(如DaVinci Machine项目,Nashorn引擎,InvokeDynamic指令,java.lang.invoke包等),推动Java虚拟机从"Java语言的虚拟机"想"多语言虚拟机"的方向发展。
2. Java发展的重大事件
3. 虚拟机与Java虚拟机
- 虚拟机
- 所谓虚拟机(Virtual Machine),就是一台虚拟的计算机,它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
- 大名鼎鼎的Visual Box,VMware就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
- 程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在java虚拟机中执行的指令我们称为Java字节码指令。
- 无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制与虚拟机提供的资源中。
- Java虚拟机
- java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的java字节码也未必由Java语言编译而成。
- JVM平台的各种语言可以共享Java虚拟机带来的跨平台型,优秀的垃圾回收器,以及科考的即时编译器。
- JAVA技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
- 作用:
JAVA虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。- 特点:
- 一次编译,到处运行。
- 自动内存管理。
- 自动垃圾回收功能
- JVM的位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互。
4. JVM的整体结构
- HotSpot VM是目前市面上高性能虚拟机的代表之一。
- 它采用解释器与即时编译器并存的架构。
- 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
谈谈你对JVM整体的理解?
- 类加载子系统
- 运行时数据区(我们核心关注这里的栈,堆,方法区)
- 执行引擎(解释器和JIT编译器共存)
5. Java代码执行流程
6. JVM的架构模型
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。
具体来说:这两种架构之间的区别:
基于栈式架构的特点:
- 涉及和实现更简单,适用于资源受限的系统。
- 避开了寄存器的分配难题:适用零地址指令方式分配。
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
- 不需要硬件支持,可移植性更好,更好实现跨平台。
基于寄存器架构的特点:
- 典型的应用是x86的二进制指令集,比如传统的PC一级Android的Davlik虚拟机。
- 指令集架构则完全依赖硬件,可移植性差。
- 性能优秀和执行更高效。
- 花费更少的指令取完成一项操作。
- 在大部分情况下,基于寄存器的指令集都以一地址指令,二地址指令和三地址指令,二基于栈式架构的指令集确实以零地址指令为主。
总结:
- 由于跨平台性的设计,Java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能涉及为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
- 时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说影视是HotSportVM的宿主环境已经不局限于嵌入式平台了),那么为什么不将架构更换为基于寄存器的架构呢?
7. JVM的生命周期
- 虚拟机的启动:
Java虚拟机的启动时通过引导类加载器(bootstrap class loader)创建一个初始类(initial class) 来完成的,这个类是由虚拟机的具体实现指定的。
- 虚拟机的运行:
- 一个运行的Java虚拟机有着一个清晰的任务:执行Java程序。
- 程序开始执行时它才运行,程序结束时它就停止。
- 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程。
- 虚拟机的退出:
有如下几种情况:
- 程序正常执行结束
- 程序在执行过程中遇到了异常或者错误而异常终止。
- 由于操作系统出现错误而导致Java虚拟机进程终止。
- 某线程调用Runtime类或者System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也运行这次exit或halt操作。
- 除此之外,JNI(Java Native InterFace)规范描述了用JNI Invocation API来加载或者卸载Java虚拟机,Java虚拟机的退出情况。
8. JVM发展历程
- Sun Classic VM
- 早在1996年Java1。0版本的时候,Sun公司发不了一款名为Sun Classic VM的Java虚拟机,它同时也是世界上第一款商用Java虚拟机,JDK1。4时完全被淘汰。
- 这块虚拟机内部只提供解释器。
- 如果使用JIT编译器,就需要进行外挂。但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作。
- 现在hotspot内置了此虚拟机。
- Exact VM
- 为了解决上一个虚拟机问题,jdk1。2时,sun提供了此虚拟机。
- Exact Memory Management:准确式内存管理。
- 也可以叫Non-Conservative/Accurate Memory Management
- 虚拟机可以知道内存中某个位置的数据具体是什么类型。
- 具体现代高性能逊尼基的雏形
- 热点探测
- 编译器与解释器混合工作模式
- 只在Solaris平台短暂使用,其他平台上还是classic VM
- 英雄气短,终被Hotspot虚拟机替换。
- Sun公司的HotSpot VM
HotSpot 历史
- 最初有一家名为"Longview Technologies"的小公司涉及。
- 1997年,此公司被Sun公司收购,2009年,Sun公司被甲骨文收购。
- JDK1。3时,HotSpot VM成为默认虚拟机。
目前HotSpot 占有绝对的市场地位,称霸武林
- 不管是现在仍在广泛是用的JDK6,还是使用比较多的JDK8中,默认的虚拟机都HotSpot。
- Sun/Oracle JDK和OpenJDK的默认虚拟机。
- 因此本课程默认介绍的虚拟机都是HotSpot 。
从服务器,桌面到移动端,嵌入式都有应用。
- 通过计数器找到最具编译价值的代码,触发即时编译或者栈上替换。
- 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡。
- BEA的JRockit
- 专注于服务器端应用
- 它可以不太关注程序启动速度,因此JRockit内部不包含解释器实现,全部代码都是靠即时编译器编译后执行。
- 大量的行业基准测试显示,JRockit JVM是世界上最快的JVM
- 使用JRockit产品,可以已经体验到了显著的西能你提高(一般超过了70%)和硬件成本的减少(50%)。
- 优势:全面的Java运行时解决方案组合
- JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或者微秒级的JVM响应时间,适合财务,军事指挥,电信网络的需要。
- MissionControl服务套件,它是一组以极低的开销来监控,管理和分析生产环境中的应用程序的工具。
- 2008年,BEA被Oracle收购。
- Oracle表达了整合两大优势虚拟机的工资,大致在JDK 8中完成。整合的方式是在HotSpot的基础上,一致JRockit的优秀特性。
- IBM的J9
- 全称: IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9。
- 市场点位与HotSpot接近,服务器端,桌面应用,嵌入式等多用途VM。
- 广泛用于IBM的各种Java产品中
- 目前,有影响力的三大商用服务器之一,也号称世界上最快的Java虚拟机。
- 2017年左右,IBM发布了开源J9 VM,命名为Open J9,交给Ecllipse基金会管理,也成为了Eclipse OpenJ9。
- KVM和CDC/CLDC Hotspot
- Oracle在Java ME产品线上的两款虚拟机为:CDC/CLDC HotSpot Implementation VM>
- KVM(Kilobyte)是CLDC-HT早起产品。
- 目前移动领域地位尴尬,只能手机被安卓和IOS二分天下。
- KVM简单,轻量,高度可移植,面向更低端的设备上还维持自己的一片市场
- 智能控制器,传感器
- 老人手机,经济欠发达地区的功能手机
- 所有的虚拟机的原则:一次编译,到处运行。
- Azul VM
- 前面三大"高性能Java虚拟机"使用在通用硬件平台上。
- 这里Azul VM和BEA Liquid VM是特定硬件平台绑定,软硬件配合的专有虚拟机
- 高性能Java虚拟机中的战斗机。
- Azul VM是Azul Systems公司在Hotspot基础上进行大量改进,运行于Azul Systems公司的专有Vega系统上的Java虚拟机。
- 每个Azul VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾> - 收集器,专有硬件优化的线程调度等有优秀特性。
- 2010年,Azul Systems公司开始从硬件转向软件,发布了自己的Zing JVM,可以在通用X86平台提供接近与Vega系统的特性。
- Liquid VM
- 高性能Java虚拟机中的战斗机。
- BEA公司开发的,直接运行在自家Hypervisor系统上。
- Liquid VM即使现在的JRockit VE,Liquid VM不需要操作系统的支持,或者说它自己本身实现了一个专有的操作系统的必要功能,如线程调度,文件系统,网络支持等。
- 随着JRockit虚拟机终止开发,Liquid VM项目也停止了。
- Apache Harmony
- Apache 也曾经推出过与JDK1。5和JDK1。6兼容的Java运行平台Apache Harmony。
- 它是IBM和Intel联合开发的开源JVM,收到同样开源的OpenJDK的压制,Sun坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转而参加OpenJDK
- 虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java类库代码吸纳进了Android SDK。
- Microsoft JVM
- 微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。
- 只能在window平台运行。但确实是当时Windows下性能最好的Java VM。
- 1997年,Sun以侵权商标,不正当竞争罪名质控微软成功,陪了sun很多钱,微软在WinowsXP SP3中抹掉了其VM。现在windows上安装的JDK都是HotSpot。
- TaobaoJVM
- 由AliJVM团队发布。阿里,国内使用Java最强大的公司,覆盖了云计算,金融,物流,电商等众多领域,需要解决高并发,高可用,分布式的复合问题。有大量的开源产品。
- 基于OpenJDK开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里Java体系的基时。
- 基于OpenJDK HotSpot VM发布的国内第一个优化,深度定制且开源的高性能服务器版Java虚拟机。
- 创新的GCIH(GC invisible heap)技术实现了off-heap,即将全生命周期较长的Java对象从heap中移到heap之外,并且GC不能管理GCIH内部的Java对象,一次达到降低GC的回收频率和提升GC的回收效率的母的。
- GCIH中的对象还能够在多个java虚拟机进程中实现共享。
- 使用crc32指令实现JVM intrinsic降低JNI的调用开销。
- PMU hardware的Java profiling tool和诊断协助工功能。
- 针对大数据场景的ZenGC。
- taobao VM应用在阿里产品上性能高,硬件严重依赖intel的cpu,损失了兼容性,但提高了性能。
目前一级在淘宝,天猫上线,把Oracle官方JVM版本全部替换了。
二、类加载子系统
- JVM架构图
1. 类加载器子系统的作用
- 类加载器子系统负责从文件系统或者忘了中心加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
2. 类加载ClassLoader角色
- class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化n个一模一样的实例。
- class file加载到JVM,被称为DNA元数据模板,放在方法区。
- 在.class文件 -->JVM -->最终成为元数据模板,此过程就要有一个运输工具(类加载器Class Loader)扮演一个快递员的角色。
3. 类加载过程
3.1 加载阶段
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 类将.class文件加载至元空间后,会在堆中创建一个java.lang.Class对象,用来封装类位于方法区内的数据结构。该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象。
补充:加载.class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar,war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用
- 从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
3.2 链接(Linking)
3.2.1 验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身的安全。
- 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
- 格式检查:是否以魔术oxCAFEBABE开头,主版本和副版本是否在当前Java虚拟机的支持范围内,数据中每一项是否都拥有正确的长度等等。
3.2.2 准备(Prepare)
- 为类变量分配内存并且设置该变量的默认初始值,即零值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。
- 注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false
3.2.3 解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程。
-事实上,解析操作往往会伴随着JVM在执行完初始化之后在执行。
-符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。
-解析动作主要针对类或者接口,字段,类方法,接口方法,方法类型等。对应常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
-符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符
解释什么是符号引号和直接引用?
- 教室里有个空的位子没坐人,座位上边牌子写着小明的座位(符号引用),后来小明进来坐下去掉牌子(符号引用换成直接引用)
- 我们去做菜,看菜谱,步骤都是什么样的(这是符号引号),当我们实际上去做,这个过程是直接引用
- 举例:输出操作System.out.println()对应的字节码:
invokevirtual #24 <java/io/PrintStream.println>
以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用
3.3 初始化(Initialization)
- 为类变量赋予正确的初始化值。
-初始化阶段就是执行类构造器clinit()的过程。
public class ClassInitTest {
private static int num=1; //类变量的赋值动作
//静态代码快中的语句
static{
num=2;
number=20;
System.out.println(num);
//System.out.println(number); 报错:非法的前向引用
}
//Linking之prepare: number=0 -->initial:20-->10
private static int number=10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);
System.out.println(ClassInitTest.number);
}
}
- 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
- clinit()不同于类的构造器。(关联:构造器是虚拟机视角下的init())
- 若该类具有父类,JVM会保证子类的clinit()执行前,父类的clinit()依据执行完毕。clinit 不同于类的构造方法(init) (由父及子,静态先行)
public class ClinitTest1 {
static class Father{
public static int A=1;
static{
A=2;
}
}
static class Son extends Father{
public static int B=A;
}
public static void main(String[] args) {
//这个输出2,则说明父类已经全部加载完毕
System.out.println(Son.B);
}
}
- 虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁。
- Java编译器并不会为所有的类都产生clinit()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含clinit()方法?
- 一个类中并没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式 (如果这个static final 不是通过方法或者构造器,则在链接阶段)
/**
* @author TANGZHI
* @create 2021-01-01 18:49
* 哪些场景下,java编译器就不会生成<clinit>()方法
*/
public class InitializationTest1 {
//场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public int num = 1;
//场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
public static int num1;
//场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public static final int num2 = 1;
}
static与final的搭配问题(使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行)
/**
* @author TANGZHI
* @create 2021-01-01
*
* 说明:使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
* 情况1:在链接阶段的准备环节赋值
* 情况2:在初始化阶段<clinit>()中赋值
* 结论:
* 在链接阶段的准备环节赋值的情况:
* 1. 对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
* 2. 对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
*
* 在初始化阶段<clinit>()中赋值的情况:
* 排除上述的在准备环节赋值的情况之外的情况。
* 最终结论:使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。
*/
public class InitializationTest2 {
public static int a = 1;//在初始化阶段<clinit>()中赋值
public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()中赋值
public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>()中赋值
public static String s2 = "helloworld2";
public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值
}
- clinit()的调用会死锁吗?
- 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕
- 正是因为函数()带锁线程安全的,因此,如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息
4. 类加载器分类
- JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
- 无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下图所示:
- 这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系
4.1 虚拟机自带的加载器
- 启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自ava.lang.ClassLoader,没有父加载器。
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
4.2 用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
用户自定义类加载器实现步骤:
- 开发人员可以通过继承抽象类ava.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader classloader1 = ClassLoader.getSystemClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classloader1);
//获取到扩展类加载器
//sun.misc.Launcher$ExtClassLoader@424c0bc4
System.out.println(classloader1.getParent());
//获取到引导类加载器 null
System.out.println(classloader1.getParent().getParent());
//获取系统的ClassLoader
ClassLoader classloader2 = Thread.currentThread().getContextClassLoader();
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classloader2);
String[]strArr=new String[10];
ClassLoader classLoader3 = strArr.getClass().getClassLoader();
//null,表示使用的是引导类加载器
System.out.println(classLoader3);
ClassLoaderDemo[]refArr=new ClassLoaderDemo[10];
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(refArr.getClass().getClassLoader());
int[]intArr=new int[10];
//null,如果数组的元素类型是基本数据类型,数组类是没有类加载器的
System.out.println(intArr.getClass().getClassLoader());
}
}
4.3 ClassLoader的使用说明
- ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
- 获取ClassLoader的途径
方式一:获取当前ClassLoader
clazz.getClassLoader()
方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
4.3 双亲委派机制
- Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
- 工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
举例
- 当我们加载jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的,所以在加载的时候,会进行双亲委派,最终从根加载器中加载 SPI核心类,然后在加载SPI接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类jdbc.jar的加载。
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
沙箱安全机制
- 自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以> - 保证对java核心源代码的保护,这就是沙箱安全机制。
- 如图,虽然我们自定义了一个java.lang包下的String尝试覆盖核心类库中的String,但是由于双亲委派机制,启动加载器会加载java核心类库的String类(BootStrap启动类加载器只加载包名为java、javax、sun等开头的类),而核心类库中的String并没有main方法
5. 其他
- 如何判断两个class对象是否相同
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
- 对类加载器的引用
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
- 类的主动使用和被动使用
- Java程序对类的使用方式分为:主动使用和被动使用。
- 主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.atguigu.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK 7 开始提供的动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果
- REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。