1. jdk, jre, jvm 关系
基本介绍:
JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
1、JDK
JDK是java开发工具包,在其安装目录下面有六个文件夹、一些描述文件、一个src压缩文件。bin、include、lib、 jre这四个文件夹起作用,demo、sample是一些例子。可以看出来JDK包含JRE,而JRE包含JVM。
bin:最主要的是编译器(javac.exe)*
include:java和JVM交互用的头文件
lib:类库
jre:java运行环境(注意:这里的bin、lib文件夹和jre里的bin、lib是不同的)
总的来说JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能。
JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。 其中的开发工具包括编译工具(javac.exe)打包工具(jar.exe)等
2、JRE
JRE是指java运行环境。光有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib。在JDK的安装目录里你可以找到jre目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。所以,在你写完java程序编译成.class之后,你可以把这个.class文件和jre一起打包发给朋友,这样你的朋友就可以运行你写程序了。
包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
3、JVM
JVM就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责将java程序生成的字节码文件解释成具体系统平台上的机器指令。让具体平台如window运行这些Java程序。
JVM:将字节码文件转成具体系统平台的机器指令。
JRE:JVM+Java语言的核心类库。
JDK:JRE+Java的开发工具。
二、了解以下未来jdk的新技术发展
- Java 13 处于创新者阶段,Java 11 处于早期采用者阶段,Java 8 处于晚期大众阶段。
Java 11 将是未来 Java 用户的最可能选项;
如果一个公司对大堆栈 GC 能力、延迟 SLA 等方面要求没有那么高,就没有足够动力去做相关升级,也未必有技术力量解决版本评估、兼容性修正等现实问题;
Java 新版本升级在中国的宣传还是不够,如果很多企业看不到技术升级的红利,势必也影响升级的积极性。
- OpenJDK 处于创新者阶段。
虽然国内很多头部厂商都在定制 OpenJDK,但是目前定制 OpenJDK 被采用范围还都有限,主体使用还是 Oracle JDK(根据《JVM 生态系统报告 2018》调查显示,70% 的开发者选择使用 Oracle JDK,21% 的开发者选择使用 OpenJDK);
厂商是否转向 OpenJDK,还有一个重要考量因素就是看他们是否愿意付费使用 OracleJDK,如果不是的话,未来 OpenJDK 可能会逐渐取代 Oracle JDK,目前国内头部厂商都在 OpenJDK 上有所动作; (对于参与 OpenJDK 的国内头部厂商来说,可能他们的看法更加积极,他们把 OpenJDK 定义在早期大众阶段)
大家在公有云、私有云等方面的竞争格局,深刻影响着在 OpenJDK 上的竞争格局;
OpenJDK 很可能被认为是一种退⽽求其次的选择。
- 非 Hotspot JDK 生产实践——Graal VM、IBM OpenJ9 处于早期采用者阶段。
Graal VM 目前还尚不可知其兼容性情况以及明确的商业化条款;
Graal VM 的部分技术,例如,基于 Java 语言开发的 JIT 引擎,可能会成为未来 OpenJDK 的基础技术;
在国内,怀疑 Graal VM、IBM OpenJ9 进入普遍生产实践的可能性会比较低。
- Lambda /Stream 处于晚期大众阶段、Vector API 处于创新者阶段。
Lambda 语法以及 Stream API 也在开发人员的⽇常⼯作中⼴泛地运用,并且没有看到语法回退的趋势;
Vector API 等前沿特性,有能力的公司有限,抑制了对其有需求的公司或者场景。
- Kotlin 处于早期大众阶段,Scala 和 Groovy 处于晚期大众阶段。
Groovy 已快成为明日黄花,往昔的光芒逐渐地被后起之秀 Kotlin 替代;
Scala 在适合的领域做王者就够了,主流不主流没那么重要;
Kotlin 被谷歌强推,谷歌支持的基本上都成功了,但是对 Kotlin 未来发展空间还是表示怀疑;
网上很多文章都在鼓吹,说 Kotlin 最终会取代 Java 成为新一代 JVM 主流语言, 但是从诞生到现在,好像依然没有语言能取代 Java。
- 微服务框架:Spring Boot 和 Spring Cloud 进入晚期大众阶段;ServiceComb 处于早期采用者阶段;Apache Dubbo 处于晚期大众阶段;Tars 处于早期大众阶段。
微服务技术处于早期大众与晚期大众之间,新的微服务开发框架需要技术突破和创新,不然已经难有一席之地;
Java 不再是微服务唯一的选择;
在技术多元化的今天,支持多语言的微服务开发框架是个必须品。
技术采用生命周期解读
在上一章节我们已经先把各位专家的观点和结论抛了出来,但是结论背后还需要很关键的原因解读,所以这一章节就按照 Java/JVM、不同层次的主流框架、微服务这三个部分,来逐一呈现。
Java/JVM
其实在 Java 版本方面,各位专家的观点完全一致:Java 13 处于创新者阶段,Java 11 处于早期采用者阶段,Java 8 处于晚期大众阶段。
在 InfoQ 面向开发者的 Java 使用版本调查中,毫无悬念,在参与问卷调研的开发者中,88.7% 正在使用 Java8 版本,这些人当中只有 35% 有升级计划,剩余 65% 并没有升级计划。
杨晓峰认为这一情况也正常:Java8 在可预见的将来依然会是生产的主体,放在晚期大众阶段是合理的。但是对于很多头部厂商来说,Java11 或者再后续版本,有可能陆续出现一定规模的生产化部署。他认为这样的趋势只会在头部公司发生,如果一个公司对大堆栈 GC 能力、延迟 SLA 等方面要求没有那么高,就没有足够动力去做相关升级,也未必有技术力量解决版本评估、兼容性修正等现实问题。所以结论就是:Java11 处于早期采用者阶段。
对此黄飞补充:也正是因为 Java11 处于早期采用者阶段,因此相关的资料较少,遇到问题会有比较高的学习成本,例如 JFR 对 11 的支持,JMC 对 Java11 的分析能力较弱。
而对于 Java 13,小马哥认为该版本在新 GC 算法的提升以及 Socket 实现上的变化还是非常令⼈期待的,因此 Java 13 排在创新者之列。
对于 Java 的升级,Oracle 宣布从 Java 9 开始每半年将更新一个 Java 大版本——Java 11 是长期支持(Long-Term -Support, LTS)版本,Java 9、10 则成了过渡版本(non‑LTS),因此,陈楚晖不建议用户在生产中使用 Java 9、10。在他看来,小版本升级相对风险是比较小的,而大版本变更则会有可能需要更改大量的代码,这也是为什么这么多人还在坚持用 Java8,而不去更新 Java 11、12、或者 13 的原因。
对于开发者升级 Java 动力不足的原因,李三红的解释更为详细,他认为有两个原因:
敏捷的基础底层架构对软件升级的支持,企业对底层架构的重视程度也是 Java 升级的一个很关键原因。中国的企业业务发展都很快,但是其实很多对底层架构的支持和重视是不足够的。底层架构是否在企业内部被统一强管控,是否很容易支持不同软件版本的灰度,并能通过有效的预发测试,覆盖软件升级不兼容等带来的不确定性,这都考验着软件升级的难度。
另外一点,如果企业享受不到技术升级带来的红利,包括性能、编程效率等多方面提升,势必也影响升级的积极性。
从此次 InfoQ 面向开发者的调研来看,对于目前 Java 的新特性和发展方向,56% 的开发者认为可以解决当前的主要业务挑战,24% 开发者的观点是不能。这也从另一层面表明:Java 经常被吐槽演进太慢,但是业界对新版本的采用并不十分积极,这可能反映了 Java/JVM 发展与开发者的实际需求存在某种脱节。
OpenJDK 定制版或者公开发行版
由于 Oracle 宣布 2019 年伊始,Oracle JDK 8 以及更⾼版本在服务器端部署不再免费,因此 OpenJDK 就成为了大多数 Java 用户的选项。根据《JVM 生态系统报告 2018》调查显示,70% 的开发者选择使用 Oracle JDK,21% 的开发者选择使用 OpenJDK。 陈楚晖也介绍了国内的情况:目前国内开发者使用最多的依旧是 Oracle JDK,其次是 IBM JDK,也有部分企业采用 OpenJDK。
对于 OpenJDK 的技术采用生命周期划分,专家们有一些观点上的不一致,杨晓峰认为虽然国内很多头部厂商都在定制 OpenJDK,但是目前定制 OpenJDK 被采用范围还都有限,这也跟上文数据结果吻合,所以他会把 OpenJDK 归在创新者阶段。
但是对于参与 OpenJDK 的国内厂商来说,可能看法更加积极。在李三红看来:厂商是否转向 OpenJDK,还有一个重要考量因素就是看他们是否愿意付费使用 OracleJDK,如果不是的话,未来 OpenJDK 可能会逐渐取代 Oracle JDK,目前国内头部厂商都在 OpenJDK 上有所动作,所以他把 OpenJDK 定义在早期大众阶段。阿里巴巴使用并开源了 OpenJDK 长期支持版本 Dragonwell,目前阿里巴巴大部分的应用运行在 Dragonwell 8, 有些已经运行在 Dragonwell 11。
据来自美团的吴革介绍:美团现阶段正在测试基于 OpenJDK 的 MtJDK,作为美团 JDK 基础服务。此外,美团主要会关注 Redhat 和 Amazon 的升级。由于 Azul 没有公开 OpenJDK 源代码,所以美团没有基于 Azul 进行研发。
3. jvm种类, 我们使用的是哪一种,特点?
我们广泛使用的是HotSpot Vm
-
Sun Classic Vm
第一款商用的虚拟机,只能使用纯解释器的方式来执行java代码。已经过时了。 -
Exact Vm
1)Exact的全称是Exact Memory Management 准确式内存管理(虚拟机可以知道内存中某个位置的数据是什么内存的)。
2)编译器和解释器混合工作以及两级即时编译器。
3)只在Solaris平台发布。还没在windows即其他平台上发布,就被HotSpotVm取代。
- HotSpot Vm
1)其实是由一家小公司开发的,后台被sun公司收购了。
2)继承了1.2款虚拟机的优点外,它还增加了热点代码探测技术等其他
3)应用最多!
- KVM
1)kilobyte简单,轻量,高度可移植
2)在手机平台运行(嵌入式领域)
5.JRockit
1)1-4都是sun公司的虚拟机,而JRockit是BEA公司研发的。不过在08年被Oracle收购,后来sun公司也被Oracle公司收购了。
2)世界上最快的java虚拟机。
3)专注服务器端应用。
4)优势:垃圾收集器;MissionControl服务套件
6.J9
1)IBM公司研发了。它最开始的名字不叫J9,叫IBM Technology for Java virtual Machine ----IT4j
2)类似于HotSpot,他不仅可以用于服务器端,还可以用于桌面应用,嵌入式;它开发是为了IBM产品的各种java平台
7.Dalvik
1)它不是java虚拟机,因为它没有遵循java虚拟机的规范,它是不能直接执行编译后的class文件的
2)它使用的是寄存器架构,而不是常用的栈架构。
3)它所执行的是Dex—dalvik Executalbe文件,这个文件可以通过class文件转化而来。
4)用于移动端----安卓
8.Microsoft JVM
1)一看就知道是微软开发的,也是为了自家软件与java兼容
2)后来被sun公司搞了,现在没了。。。。。
9.Azul VM 和 Liquid VM(两款高性能JVM,碾压HotSpot)
1)像LiquidVM不需要操作系统的支持,它本身就是一个操作系统。我们总是说java慢,是因为运行java代码时,我们要先进过java虚拟机,再通过虚拟机调操作系统,多了一步。
10.TaobaoVM
1)淘宝根据Hotspot进行深度定制的的虚拟机
2)对硬件的依赖性够,牺牲了兼容性。
4. 为什么叫java 虚拟机,它与 vmware的区别?
Java虚拟机”(缩写为JVM)是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。它有自己完善的硬件架构(如处理器、堆栈、寄存器等),还具有相应的指令系统。使用“Java虚拟机”程序就是为了支持与操作系统无关、在任何系统中都可以运行的程序。微软公司出于竞争策略考虑,在Windows XP中不捆绑JVM,所以只能上网下载。
VM(Virtual Manufacturing ) 主机其实就是VMware主机的简称。VM 虚拟制造:其本质是以新产品及其制造系统的全局最优化为目标,以计算机支持的仿真技术为前提,对设计、制造等生产过程进行统一建模,在产品设计阶段,实时地、并行地模拟出产品未来制造全过程及其对产品设计的影响,预测产品性能、产品制造成本、产品的可行制造等。
5、java虚拟机的整体架构
•JVM(虚拟机):指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现。常用的虚拟机有VMWare,Virtual Box,Java Virtual Machine
•Java虚拟机阵营:Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、Azul VM、Apache Harmony、Google Dalvik VM、Microsoft JVM…
6. 字节码的加载流程?
•JVM由三个主要的子系统构成
•类加载器子系统
•运行时数据区(内存结构)
•执行引擎
•Java运行时编译源码(.java)成字节码,由jre运行。jre由java虚拟机(jvm)实现。Jvm分析字节码,后解释并执行
.类加载器
-
负责从文件系统(简单说就是硬盘)或者网络上加载class文件,class文 件在文件开头有特点的标识。
-
ClassLoader只负责class文件的加载,只要是符合JVM对字节码文件规范的要求就可以,至于它是否可以运行,由执行引擎决定。
-
加载的类信息存放在方法区(一块内存空间),除了类信息之外,方法区中还会存放运行时常量池信息,可能还会包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)
例如一个磁盘上的xxx.class文件,会被JVM的ClassLoader(有很多类加载器)通过二进制流的方式,加载到JVM中,此时会生成对应的xxx的class对象,通过该class对象就可以调用构造函数生成xxx的对象放在方法区当中。
类加载器加载字节码文件主要分为三个阶段:
加载字节码文件
通过一个类的路径,加载此class文件的二进制字节流将这个字节流所代表的静态存储结构转换为方法区(方法区具体来说,jdk7之前叫永久代,之后叫元数据或者元空间,泛称为方法区)的运行时数据结构
在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类各种数据的访问入口
加载字节码文件的方式:
从系统磁盘直接读取
通过网络获取,如:Web Applet(我没有了解过~~~)
从zip压缩包中读取,典型的就是jar包和war包,这些压缩包中都是已经编译好的字节码文件
运行时生成,如:动态代理(java.lang.reflect)
其他文件生成,典型的形式如:JSP
从专用的数据库中提取class字节码文件(我没有了解过~~~)
链接 验证
确保加载的class字节码文件内容信息是符合当前JVM规范的,保证被加载类的正确性,不会有安全风险
包括文件格式验证、元数据验证、字节码验证、符号引用验证(了解一下就好,如校验二进制文件头是否是:CA FE BA BE,CA FE BA BE是JAVA虚拟机识别的标识符)
准备
为类变量分配内存,并且设置变量的初始值(0、false或者null等)
//在链接阶段的准备过程中,a会被赋值为0,1的赋值会在初始化阶段中进行
int a = 1;
这里不包含final修饰的static(也就是常量),因为final修饰的常量在编译的时候就会分配值了,准备阶段会显式的初始化
不会为实例变量初始化(未被static修饰的)成员变量,类变量会被分配到方法区中,实例变量随对象创建后分配到JVM的堆中
解析
将常量池中的符号引用转换为直接引用的过程
符号引用就是使用符号来描述所引用的目标(符号引用的字面量形式定义在《Java虚拟机规范》中的class文件格式,可以理解为一个符号从字面上定义了引用的目标),直接引用就是直接指向目标的指针、相对偏移量、或者一个间接定位到的目标句柄
解析的目标主要针对:类、接口、字段、类方法、接口方法、方法类型等。对应常量池当中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodred_info。(这一块不是很懂)
初始化
初始化阶段就是执行类构造方法()的过程
构造器方法()中的指令,按照源文件中代码出现的顺序执行(其实java的IDE会有相应的语法检查的,不会让你在static静态代码块中调用静态代码块之后声明的变量的)
该方法是定义好的,不是类的构造方法,构造方法对应的class文件中()方法,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来(简单的理解就是()是在编译的时候根据类变量赋值操作代码和类静态代码块中的代码拼接出来的方法,如果类中没有定义静态变量或者是静态代码块那么就不会有()方法产生)
七、 java的编译器输入的指令流是一种基于栈的指令集架构, 它有什么优点?
主要特点:
设计实现简单,适用于资源受限的系统,比如机顶盒,小玩具上。
避开寄存器分配难题:使用零地址指令方式分配。
指令流中大部分都是零地址指令,执行过程依赖操作栈,指令集更小(零地址),编译器容易实现。
不需要硬件支持,可移植性强,容易实现跨平台。
八、能运行在虚拟机上的字节码只能由 javac 编译而来的java源代码产生吗, 除此之外,还有其它哪些语言也可以编译字节码出来?
编译型语言
编译型语言,在程序执行之前,需要一个专门的编译链接过程,把程序编译成机器语言文件;比如,exe文件和bin文件。以后运行的话就不用重新编译了,直接使用编译的结果就行了。因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高!
常见的编译型语言有:C、C++、Pascal、Object Pascal和Delphi等。
九、 能简单的说说java虚拟机规范吗?
========================================================
Part Two 类加载相关
1. jvm在什么情况下会加载一个类?
类加载过程
加载——> 验证 —>准备—> 解析—> 初始化—> 使用—> 卸载
在使用的时候会加载这个类
public class Hello(){
public static void mian (){
}
}
解析
解析阶段就是把符号引用转换直接引用
初始化
什么是类初始化?
new getstatic putstatic invokestatic字节指令
反射调用该类
初始化一个类会连带把父类初始化
执行main方法的类
使用jdk动态语言支持
2. 类加载到jvm中的过程?每个阶段的工作?
3. jvm中的类加载器的类型及它加载的目标路径?如何自定义一个类加载器加载一个指定目录下的class文件?
1、类加载器
站在Java虚拟机的角度看,只有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机、JDK8中),是虚拟机自身的一部分;另外一种是其他所有类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader
JDK8及以前版本中绝大多数程序都会使用到以下3个系统提供的类加载器来进行加载
启动类(引导类)加载器
负责加载支撑JVM运行的位于<JAVA_HOME>\lib目录下的核心类库,而且是Java虚拟机能够识别的类库加载到虚拟机内存中(如rt.jar、tools.jar、charsets.jar等,名字不符合的类库即使放到lib目录下也不会被加载)。
1.引导类加载器使用C/C++语言实现,在JVM内部
2.用于加载Java核心类库
3.不继承ClassLoader
4.还用于加载扩展类加载器和应用程序类加载器
5.只加载包名为java,javax,sun开头的类
扩展类加载器(Extension Class Loader)
这个类加载器是在类sun.misc. $ExtClassLoader中以Java代码的形式实现的。负责加载<JAVA_HOME>\lib\ext目录中或被java.ext.dirs系统变量所制定的路径中所有的类库,是一种Java系统类库的扩展机制
1.使用java语言编写,JVM自带
2.继承自ClassLoader
3.父类加载器为启动类加载器
4.从java.ext.dirs指定的路径下加载类库;或者从JDK安装目录的jre/lib/ext目录下加载类库。
5.如果用户自定义的jar包放在jre/lib/ext下,也会自动由扩展类加载器加载
应用程序类加载器(Application Class Loader)
是由sun.misc.launcher$AppClassLoader来实现,由于应用程序加载器是ClassLoader类中getSystemClassLoader()方法的返回值,也称为系统类加载器。负责加载用户类路径(ClassPath)上所有的类库,如应用程序中没有默认自己的类加载器,则使用应用程序加载器为默认加载器。
自定义加载器:负责加载用户自定义路径下的类包
1.使用jaca语言编写,JVM自带
2.继承自ClassLoader
3.父类加载器为扩展类加载器
4.负责加载环境变量classpath或系统属性java.class.path指定的类库
5.java中自己写的类都是由应用程序类加载器加载的
6.可以通过ClassLoader.getSystemClassLoader()方法获取该类加载器
理解BootstrapClassLoader、ExtClassLoader、AppClassLoader的例子:
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();//获取到的是通过引导类加载的类库的路径(输出参考“引导类能够加载的类库路径”)
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader); //输出为null。说明是引导类加载器加载的
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {// 输出参考“扩展类能够加载的类库路径”图
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
自定义一个类加载器简单的例子:
public class CustomClassLoader extends ClassLoader { //继承ClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException { //重载findClass方法
try {
byte[] result = getClassFromCustomPath(name);
if(result == null){
throw new FileNotFoundException();
}else{
return defineClass(name,result,0,result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name){
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。(这里可以进行解密操作,防止class文件被反编译)
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One",true,customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. 什么是双亲委派模型,有什么作用?
双亲委派模型有两个好处:
向上委托给父类加载,父类加载不了再自己加载
避免重复加载,防止Java核心api被篡改
加载器自上而下分别为,启动类加载器(Bootstrap ClassLoader), 拓展类加载器(Extension ClassLoader), 系统类加载器(Application ClassLoader) , 自定义类加载器(Custom ClassLoader)
双亲委派模式是Java1.2之后引入的,其工作原理是,如果其中一个类加载器收到了类加载的请求,它并不会自己去加载而是会将该请求委托给父类的加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,如此递归,请求最终到达顶层的启动类加载器。如果父类能加载,则直接返回,如果父类加载不了则交由子类加载,这就是双亲委派模式。
5. 类加载器是如何确定一个类在jvm中的唯一性的? 两个类来源于同一个Class文件,被同一个虚拟机加载,这两个类一定相等吗?
最近遇到了一个问题:由不同类加载器加载同一个类,实例化为对象。使用instanceof判断该对象与该类的归属,请问结果是true还是false? 答案是false。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof jvm.ClassLoaderTest);
}
}
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
两行输出结果中,从第一句可以看出,这个对象确实是类 Practice.Java.ClassLoaderTest实例化出来的对象,但从第二句可以发现,这个对象与类Practice.Java.ClassLoaderTest做所属类型检查的时候却返回了false。
这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的。虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然为false。
结论
Java类加载器这种特性可以简单的总结为命名空间。即在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。**
6. tomcat的类加载器有哪些?
Tomcat中的类加载器
下面的简图是Tomcat9版本的官方文档给出的Tomcat的类加载器的图
1.Bootstrap:是java中的最高加载器,用C语言实现,主要用来加载JVM启动时所需 要的核心类,如:$JAVA_HOME/jre/lib/ext
3.System:会加载CLASSPATH系统变量所定义路径的所有类
4.Common:会加载Tomcat路径下的lib文件下的所有类
5.Webapp1,Webapp2••••会加载APP路径下项目中的所有的类,一个项目对应一个6.WabappClassLoader,这样就实现了应用之间类的隔离
这3个部分,在上面的Java双亲委派模型图中都有体现。不过可以看到ExtClassLoader没有画出来,可以理解为是跟bootstrap合并了,都是去JAVA_HOME/jre/lib下面加载类。 那么Tomcat为什么要自定义类加载器呢?
隔离不同应用:部署在同一个Tomcat中的不同应用A和B,例如A用了Spring2.5。B用了Spring3.5,那么这两个应用如果使用的是同一个类加载器,那么Web应用就会因为jar包覆盖而无法启动。
灵活性:Web应用之间的类加载器相互独立,那么就可以根据修改不同的文件重建不同的类加载器替换原来的。从而不影响其他应用。
性能:如果在一个Tomcat部署多个应用,多个应用中都有相同的类库依赖。那么可以把这相同的类库让Common类加载器进行加载。
Tomcat自定义了WebAppClassLoader类加载器
打破了双亲委派的机制,即如果收到类加载的请求,会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类。我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat既然要打破这个规则,就要重写loadClass方法,我们可以看WebAppClassLoader类中重写的loadClass方法
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 1. 从本地缓存中查找是否加载过此类
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 2. 从AppClassLoader中查找是否加载过此类
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
String resourceName = binaryNameToPath(name, false);
// 3. 尝试用ExtClassLoader 类加载器加载类,防止Web应用覆盖JRE的核心类
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
tryLoadingFromJavaseLoader = true;
}
boolean delegateLoad = delegate || filter(name, true);
// 4. 判断是否设置了delegate属性,如果设置为true那么就按照双亲委派机制加载类
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 5. 默认是设置delegate是false的,那么就会先用WebAppClassLoader进行加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 6. 如果此时在WebAppClassLoader没找到类,那么就委托给AppClassLoader去加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
Web应用默认的类加载顺序是(打破了双亲委派规则):
先从JVM的BootStrapClassLoader中加载。
加载Web应用下/WEB-INF/classes中的类。
加载Web应用下/WEB-INF/lib/.jap中的jar包中的类。
加载上面定义的System路径下面的类。
加载上面定义的Common路径下面的类。
如果在配置文件中配置了,那么就是遵循双亲委派规则,加载顺序如下:
. 先从JVM的BootStrapClassLoader中加载。
. 加载上面定义的System路径下面的类。
. 加载上面定义的Common路径下面的类。
. 加载Web应用下/WEB-INF/classes中的类。
. 加载Web应用下/WEB-INF/lib/.jap中的jar包中的类
8. 双亲委派模型最大问题:底层的类加载器无法加载底层的类, 比如如下情况:
javax.xml.parsers包中定义了xml解析的类接口, Service Provider Interface SPI 位于rt.jar
即接口在启动ClassLoader中, 而SPI的实现类,通常是由用户实现的, 由AppLoader加载。
以下是javax.xmlparsers.FactoryFinder中的解决代码:
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
cl = ss.getContextClassLoader(); //获取上下文加载器
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return cl.loadClass(className); //使用上下文ClassLoader
}
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}
更多可以参考理解: jdbc的SPI 加载方式. https://blog.csdn.net/syh121/article/details/120274044
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
9. 双亲委派模式是默认的模式,但并非必须. 还有以下几个例 子,它实际上是破坏了双亲委派模式的.
a. Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent b. OSGi的ClassLoader形成网状结构,根据需要自由加载Class
10. 请完成一个热替换的例子,并解释什么是热替换?
jvm中只有主动使用类,才会加载类,那么加载类的七种情况有哪些?
类加载过程主要分为三个步骤:加载、链接、初始化,而其中链接过程又分为三个步骤:验证、准备、解析,加上卸载、使用两个步骤统称为为类的生命周期。
加载
简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
1、字节码来源
由于没有具体指明需要在哪里获取class文件,导致字节码来源途径非常丰富:
从压缩包中读取,如jar、war
从网络中获取,如Web Applet
动态生成,如动态代理、CGLIB
由其他文件生成,如JSP
从数据库读取
从加密文件中读取
2、内存储存
将静态储存解析成运行时数据,存放在方法区
在堆区生成该类的Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证阶段主要是为了为了确保Class文件的字节流中包含的信息符合虚拟机要求,并且不会危害虚拟机。
而验证主要分为以下四类:
文件格式验证
元数据验证
字节码验证
符号引用验证