# jvm学习
## JVM与Java体系结构
![image-20200704111417472](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/23/185714-962218.png)
### 字节码
我们平时说的java字节码,指的是用java语言编译成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的。所以应该统称为:jvm字节码。
不同的编译器,可以编译出相同的字节码文件,字节码文件也可以在不同的JVM上运行。
Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式—Class文件格式所关联,Class文件中包含了Java虚拟机指令集(或者称为字节码、Bytecodes)和符号表,还有一些其他辅助信息。
### 多语言混合编程
Java平台上的多语言混合编程正成为主流,通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。
试想一下,在一个项目之中,并行处理用clojure语言编写,展示层使用JRuby/Rails,中间层则是Java,每个应用层都将使用不同的编程语言来完成,而且,接口对每一层的开发者都是透明的,各种语言之间的交互不存在任何困难,就像使用自己语言的原生API一样方便,因为它们最终都运行在一个虚拟机之上。
对这些运行于Java虚拟机之上、Java之外的语言,来自系统级的、底层的支持正在迅速增强,以JSR-292为核心的一系列项目和功能改进(如Da Vinci Machine项目、Nashorn引擎、InvokeDynamic指令、java.lang.invoke包等),推动Java虚拟机从“Java语言的虚拟机”向 “多语言虚拟机”的方向发展。
### Java发展的重大事件
- 1990年,在Sun计算机公司中,由Patrick Naughton、MikeSheridan及James Gosling领导的小组Green Team,开发出的新的程序语言,命名为oak,后期命名为Java
- 1995年,Sun正式发布Java和HotJava产品,Java首次公开亮相。
- 1996年1月23日sun Microsystems发布了JDK 1.0。
- 1998年,JDK1.2版本发布。同时,sun发布了JSP/Servlet、EJB规范,以及将Java分成了J2EE、J2SE和J2ME。这表明了Java开始向企业、桌面应用和移动设备应用3大领域挺进。
- 2000年,JDK1.3发布,Java HotSpot Virtual Machine正式发布,成为Java的默认虚拟机。
- 2002年,JDK1.4发布,古老的Classic虚拟机退出历史舞台。
- 2003年年底,Java平台的scala正式发布,同年Groovy也加入了Java阵营。
- 2004年,JDK1.5发布。同时JDK1.5改名为JavaSE5.0。
- 2006年,JDK6发布。同年,Java开源并建立了openJDK。顺理成章,Hotspot虚拟机也成为了openJDK中的默认虚拟机。
- 2007年,Java平台迎来了新伙伴Clojure。
- 2008年,oracle收购了BEA,得到了JRockit虚拟机。
- 2009年,Twitter宣布把后台大部分程序从Ruby迁移到scala,这是Java平台的又一次大规模应用。
- 2010年,oracle收购了sun,获得Java商标和最真价值的HotSpot虚拟机。此时,oracle拥有市场占用率最高的两款虚拟机HotSpot和JRockit,并计划在未来对它们进行整合:HotRockit
- 2011年,JDK7发布。在JDK1.7u4中,正式启用了新的垃圾回收器G1。
- 2017年,JDK9发布。将G1设置为默认Gc,替代CMS
- 同年,IBM的J9开源,形成了现在的open J9社区
- 2018年,Android的Java侵权案判决,Google赔偿oracle计88亿美元
- 同年,oracle宣告JavagE成为历史名词JDBC、JMS、Servlet赠予Eclipse基金会
- 同年,JDK11发布,LTS版本的JDK,发布革命性的zGc,调整JDK授权许可
- 2019年,JDK12发布,加入RedHat领导开发的shenandoah GC
### 虚拟机与Java虚拟机
#### 虚拟机
所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
- 大名鼎鼎的Visual Box,Mware就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
- 程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。
无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。
#### Java虚拟机
Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
特点:
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收功能
### JVM的位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互
![image-20200704183048061](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/24/182006-599099.png)
Java的体系结构
![image-20200704183236169](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/24/181958-162957.png)
### JVM整体结构
- HotSpot VM是目前市面上高性能虚拟机的代表作之一。
- 它采用解释器与即时编译器并存的架构。
- 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
![image-20200704183436495](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/24/181953-876181.png)
执行引擎包含三部分:解释器,及时编译器,垃圾回收器
### Java代码执行流程
![image-20200704210429535](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/24/182103-605478.png)
只是能生成被Java虚拟机所能解释的字节码文件,那么理论上就可以自己设计一套代码了
### JVM的架构模型
Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。具体来说:这两种架构之间的区别:
基于栈式架构的特点
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题:使用零地址指令方式分配。
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
- 不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点
- 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
- 指令集架构则完全依赖硬件,可移植性差
- 性能优秀和执行更高效
- 花费更少的指令去完成一项操作。
- 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主方水洋
#### 举例
同样执行2+3这种逻辑操作,其指令分别如下:
基于栈的计算流程(以Java虚拟机为例):
```java
iconst_2 //常量2入栈
istore_1
iconst_3 // 常量3入栈
istore_2
iload_1
iload_2
iadd //常量2/3出栈,执行相加
istore_0 // 结果5入栈
```
而基于寄存器的计算流程
```java
mov eax,2 //将eax寄存器的值设为2
add eax,3 //使eax寄存器的值加3
```
### 字节码反编译
我们编写一个简单的代码,然后查看一下字节码的反编译后的结果
```java
public class Fanbianyi {
public static void main(String[] args) {
int a = 1 + 2;
System.out.println(a);
}
}
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//idea显示的经过编译后的类
package code;
public class Fanbianyi {
public Fanbianyi() {
}
public static void main(String[] args) {
int a = 3;
System.out.println(a);
}
}
```
```bash
Last modified 2021年3月24日; size 529 bytes
MD5 checksum 75c4c50b1c4a3767358254fcb97b1d62
Compiled from "Fanbianyi.java"
public class code.Fanbianyi
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // code/Fanbianyi
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Fieldref #22.#23 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #24.#25 // java/io/PrintStream.println:(I)V
#4 = Class #26 // code/Fanbianyi
#5 = Class #27 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcode/Fanbianyi;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 SourceFile
#20 = Utf8 Fanbianyi.java
#21 = NameAndType #6:#7 // "<init>":()V
#22 = Class #28 // java/lang/System
#23 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(I)V
#26 = Utf8 code/Fanbianyi
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (I)V
{
public code.Fanbianyi();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcode/Fanbianyi;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_3
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
2 8 1 a I
}
SourceFile: "Fanbianyi.java"
```
### 总结
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说应该是HotSpotVM的宿主环境已经不局限于嵌入式平台了),那么为什么不将架构更换为基于寄存器的架构呢?
### 栈
- 跨平台性
- 指令集小
- 指令多
- 执行性能比寄存器差
### 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虚拟机的退出情况。
### 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虚拟机替换
#### HotSpot VM
HotSpot历史
- 最初由一家名为“Longview Technologies”的小公司设计
- 1997年,此公司被sun收购;2009年,Sun公司被甲骨文收购。
- JDK1.3时,HotSpot VM成为默认虚拟机
目前Hotspot占有绝对的市场地位,称霸武林。
- 不管是现在仍在广泛使用的JDK6,还是使用比例较多的JDK8中,默认的虚拟机都是HotSpot
- Sun/oracle JDK和openJDK的默认虚拟机
- 因此本课程中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的Gc机制。(比如其他两个商用虚机都没有方法区的概念)
从服务器、桌面到移动端、嵌入式都有应用。
名称中的HotSpot指的就是它的热点代码探测技术。
- 通过计数器找到最具编译价值代码,触发即时编译或栈上替换
- 通过编译器与解释器协同工作,在最优化的程序响应时间与最佳执行性能中取得平衡
#### JRockit
专注于服务器端应用
- 它可以不太关注程序启动速度,因此JRockit内部不包含解析器实现,全部代码都靠即时编译器编译后执行。
大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。
- 使用JRockit产品,客户已经体验到了显著的性能提高(一些超过了70%)和硬件成本的减少(达50%)。
优势:全面的Java运行时解决方案组合
- JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒级的JVM响应时间,适合财务、军事指挥、电信网络的需要
- MissionControl服务套件,它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。
2008年,JRockit被oracle收购。
oracle表达了整合两大优秀虚拟机的工作,大致在JDK8中完成。整合的方式是在HotSpot的基础上,移植JRockit的优秀特性。
高斯林:目前就职于谷歌,研究人工智能和水下机器人
#### IBM的J9
全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号:J9
市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM广泛用于IBM的各种Java产品。
目前,有影响力的三大商用虚拟机之一,也号称是世界上最快的Java虚拟机。
2017年左右,IBM发布了开源J9VM,命名为openJ9,交给EClipse基金会管理,也称为Eclipse OpenJ9
OpenJDK -> 是JDK开源了,包括了虚拟机
#### KVM和CDC / CLDC Hotspot
oracle在Java ME产品线上的两款虚拟机为:CDC/CLDC HotSpot Implementation VM KVM(Kilobyte)是CLDC-HI早期产品目前移动领域地位尴尬,智能机被Angroid和ioS二分天下。
KVM简单、轻量、高度可移植,面向更低端的设备上还维持自己的一片市场
- 智能控制器、传感器
- 老人手机、经济欠发达地区的功能手机
所有的虚拟机的原则:一次编译,到处运行。
#### Azul VM
前面三大“高性能Java虚拟机”使用在通用硬件平台上这里Azu1VW和BEALiquid VM是与特定硬件平台绑定、软硬件配合的专有虚拟机I
- 高性能Java虚拟机中的战斗机。
Azul VM是Azu1Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件Vega系统上的ava虚拟机。
每个Azu1VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。
2010年,AzulSystems公司开始从硬件转向软件,发布了自己的zing JVM,可以在通用x86平台上提供接近于Vega系统的特性。
#### Liquid VM
高性能Java虚拟机中的战斗机。
BEA公司开发的,直接运行在自家Hypervisor系统上Liquid VM即是现在的JRockit VE(Virtual Edition),
Liquid VM不需要操作系统的支持,或者说它自己本身实现了一个专用操作系统的必要功能,如线程调度、文件系统、网络支持等。
随着JRockit虚拟机终止开发,Liquid vM项目也停止了。
#### Apache Marmony
Apache也曾经推出过与JDK1.5和JDK1.6兼容的Java运行平台Apache Harmony。
它是IElf和Inte1联合开发的开源JVM,受到同样开源的openJDK的压制,Sun坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转而参与OpenJDK
虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java类库代码吸纳进了Android SDK。
#### Micorsoft JVM
微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。
只能在window平台下运行。但确是当时Windows下性能最好的Java VM。
1997年,sun以侵犯商标、不正当竞争罪名指控微软成功,赔了sun很多钱。微软windowsXPSP3中抹掉了其VM。现在windows上安装的jdk都是HotSpot。
#### Taobao JVM
由AliJVM团队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。
基于openJDK开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里Java体系的基石。
基于openJDK Hotspot VM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机。
- 创新的GCIH(GCinvisible 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应用在阿里产品上性能高,硬件严重依赖inte1的cpu,损失了兼容性,但提高了性能
目前已经在淘宝、天猫上线,把oracle官方JvM版本全部替换了。
#### Dalvik VM
谷歌开发的,应用于Android系统,并在Android2.2中提供了JIT,发展迅猛。
Dalvik y只能称作虚拟机,而不能称作“Java虚拟机”,它没有遵循 Java虚拟机规范
不能直接执行Java的Class文件
基于寄存器架构,不是jvm的栈架构。
执行的是编译以后的dex(Dalvik Executable)文件。执行效率比较高。
- 它执行的dex(Dalvik Executable)文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。
Android 5.0使用支持提前编译(Ahead of Time Compilation,AoT)的ART VM替换Dalvik VM。
#### Graal VM
2018年4月,oracle Labs公开了GraalvM,号称 "Run Programs Faster Anywhere",勃勃野心。与1995年java的”write once,run anywhere"遥相呼应。
GraalVM在HotSpot VM基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”
的运行平台使用。语言包括:Java、Scala、Groovy、Kotlin;C、C++、Javascript、Ruby、Python、R等
支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件
工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示。Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率。
如果说HotSpot有一天真的被取代,Graalvm希望最大。但是Java的软件生态没有丝毫变化。
#### 总结
具体JVM的内存结构,其实取决于其实现,不同厂商的JVM,或者同一厂商发布的不同版本,都有可能存在一定差异。主要以oracle HotSpot VM为默认虚拟机
## 类加载子系统
### 概述
![image-20200705080719531](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/121111-609261.png)
#### 总体结构
![image-20200705080911284](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/121144-683827.png)
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
### 类加载器子系统作用
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
![image-20200705081813409](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/121213-567250.png)
- class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file加载到JVM中,被称为DNA元数据模板,放在方法区。
- 在.class文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
![image-20200705081913538](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/122315-602866.png)
### 类的加载过程
例如下面的一段简单的代码
```java
public class HelloLoader {
public static void main(String[] args) {
System.out.println("我已经被加载啦");
}
}
```
![image-20200705082255746](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/122422-987911.png)
完整的流程图如下所示
![image-20200705082601441](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/122442-826564.png)
### 加载阶段
通过一个类的全限定名获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
#### 加载class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
#### 链接阶段
##### 验证 Verify
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
> 工具:Binary Viewer查看
![image-20200705084038680](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/122550-957453.png)
如果出现不合法的字节码文件,那么将会验证不通过
同时我们可以通过安装IDEA的插件,来查看我们的Class文件
![image-20210326123611201](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/123614-605345.png)
安装完成后,我们编译完一个class文件后,点击view即可显示我们安装的插件来查看字节码方法了
##### 准备 Prepare
为类变量分配内存并且设置该类变量的默认初始值,即零值。
```java
public class HelloApp {
private static int a = 1; // 准备阶段为0,在下个阶段,也就是初始化的时候才是1
public static void main(String[] args) {
System.out.println(a);
}
}
```
上面的变量a在准备阶段会赋初始值,但不是1,而是0。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
##### 解析 Resolve
将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
#### 初始化阶段
初始化阶段就是**执行类构造器法<clinit>()**的过程。
**此方法不需定义**,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 也就是说,当我们代码中**包含static变量**的时候,就会有clinit方法
构造器方法中指令按语句在源文件中出现的顺序执行。
<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
- 任何一个类在声明后,都有生成一个构造器,默认是空参构造器
```java
public class ClassInitTest {
private static int num = 1;
static {
num = 2;
number = 20;
System.out.println(num);
System.out.println(number); //报错,非法的前向引用
}
private static int number = 10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num); // 2
System.out.println(ClassInitTest.number); // 10
}
}
```
关于涉及到父类时候的变量赋值过程
```java
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) {
System.out.println(Son.b); // 2
}
}
```
我们输出结果为 2,也就是说首先加载ClinitTest1的时候,会找到main方法,然后执行Son的初始化,但是Son继承了Father,因此还需要执行Father的初始化,同时将A赋值为2。我们通过反编译得到Father的加载过程,首先我们看到原来的值被赋值成1,然后又被复制成2,最后返回
```bash
0 iconst_1
1 putstatic #2 <code/ClinitTest1$Father.A>
4 iconst_2
5 putstatic #2 <code/ClinitTest1$Father.A>
8 return
```
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。
```java
public class DeadThreadTest {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程t1开始");
new DeadThread();
}, "t1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 线程t2开始");
new DeadThread();
}, "t2").start();
}
}
class DeadThread {
static {
if (true) {
System.out.println(Thread.currentThread().getName() + "\t 初始化当前类");
while(true) {
}
}
}
}
```
上面的代码,输出结果为
```bash
t2 线程t2开始
t1 线程t1开始
t2 初始化当前类
```
从上面可以看出初始化后,只能够执行一次初始化,这也就是同步加锁的过程
### 类加载器的分类 java8
java11版本已经出现改变 但是总体架构应该没有发生很大的变化
JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
![image-20200705094149223](F:/project/github/otherClone/LearningNotes/JVM/1_内存与垃圾回收篇/2_类加载子系统/images/image-20200705094149223.png)
这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。
我们通过一个类,获取它不同的加载器
#### 启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVAHOME/jre/1ib/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放在此目录下,也会自动由扩展类加载器加载。
#### 应用程序类加载器(系统类加载器,AppClassLoader)
- javI语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过classLoader#getSystemclassLoader()方法可以获取到该类加载器
#### 用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄漏
用户自定义类加载器实现步骤:
- 开发人员可以通过继承抽象类ava.1ang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写1oadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖1oadclass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
#### 查看根加载器所能加载的目录
刚刚我们通过概念了解到了,根加载器只能够加载 java /lib目录下的class,我们通过下面代码验证一下
```java
/**
* @author: 陌溪
* @create: 2020-07-05-10:17
*/
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("*********启动类加载器************");
// 获取BootstrapClassLoader 能够加载的API的路径
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
// 从上面路径中,随意选择一个类,来看看他的类加载器是什么:得到的是null,说明是 根加载器
ClassLoader classLoader = Provider.class.getClassLoader();
}
}
```
得到的结果
```
*********启动类加载器************
file:/E:/Software/JDK1.8/Java/jre/lib/resources.jar
file:/E:/Software/JDK1.8/Java/jre/lib/rt.jar
file:/E:/Software/JDK1.8/Java/jre/lib/sunrsasign.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jsse.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jce.jar
file:/E:/Software/JDK1.8/Java/jre/lib/charsets.jar
file:/E:/Software/JDK1.8/Java/jre/lib/jfr.jar
file:/E:/Software/JDK1.8/Java/jre/classes
null
```
#### 关于ClassLoader
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
![image-20200705103516138](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/142346-195086.png)
sun.misc.Launcher 它是一个java虚拟机的入口应用
![image-20200705103636003](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/142345-705502.png)
获取ClassLoader的途径
- 获取当前ClassLoader:clazz.getClassLoader()
- 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
- 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
- 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()
#### 静态代码只会被加载一次
```java
public class LoadClass {
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) {
System.out.println(Son.B);
}
}
```
![image-20210305125017468](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/05/125020-34240.png)
```java
public class LoadClass {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 系统类加载器 jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
// 父类(扩展类的类加载器)的加载器 jdk.internal.loader.ClassLoaders$PlatformClassLoader@2133c8f8
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader);
// 输出为null
// 用户自定义类加载器 默认使用系统类加载器
System.out.println(LoadClass.class.getClassLoader());
// 系统类加载器 jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
// String类是使用引导类加载器进行加载的 --> java的核心类库都是使用引导类加载器
System.out.println(String.class.getClassLoader());
}
}
```
### 双亲委派机制
Java虚拟机对Class文件采用的是按需加载,而且加载class文件时,Java虚拟机使用的是双亲委派模式,即把请求交由父类处理,它是异种任务委派模式
1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载。而是把这个请求委托给父类的加载器去执行
2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将达到顶层的启动类加载器
3. 如果父类的加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
![image-20200705105810107](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/142700-500377.png)
#### 沙箱安全机制
自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
#### 双亲委派机制的优势
通过上面的例子,我们可以知道,双亲机制可以
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
优点有两个,1. 避免类的重复加载 2.保护程序安全,防止核心API被篡改
### 其它
#### 如何判断两个class对象是否相同
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- **类的完整类名必须一致**,包括包名。
- **加载这个类的ClassLoader**(指ClassLoader实例对象)必须**相同**。
换句话说,在JvM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
#### 类的主动使用和被动使用
Java程序对类的使用方式分为:王动使用和被动使用。
主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法I
- 反射(比如:Class.forName("com.atguigu.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK7开始提供的动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
## 运行时数据区概述及线程
### 前言
本节主要讲的是运行时数据区,也就是下图这部分,它是在类加载完成后的阶段
![image-20200705111640511](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/143821-98505.png)
当我们通过前面的:类的加载-> 验证 -> 准备 -> 解析 -> 初始化 这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区
![image-20200705111843003](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/143800-513319.png)
也就是大厨做饭,我们把大厨后面的东西(切好的菜,刀,调料),比作是运行时数据区。而厨师可以类比于执行引擎,将通过准备的东西进行制作成精美的菜品
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局
> 我们通过磁盘或者网络IO得到的数据,都需要先加载到内存中,然后CPU从内存中获取数据进行读取,也就是说内存充当了CPU和磁盘之间的桥梁
运行时数据区的完整图
![image-20200705112416101](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/143914-594351.png)
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
灰色的为单独线程私有的,红色的为多个线程共享的。即:
- 每个线程:独立包括程序计数器、栈、本地栈。
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
![image-20200705112601211](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/143759-336054.png)
### 线程
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
- 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。
#### JVM系统线程
如果你使用console或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。|
这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
- 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
- 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
- GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
- 编译线程:这种线程在运行时会将字节码编译成到本地代码。
- 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。
## 程序计数器
### 介绍
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或**指令计数器**)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。**JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。**
![image-20200705155551919](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/144124-853570.png)
它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
在JVM规范中,**每个线程都有它自己的程序计数器**,是线程私有的,**生命周期与线程的生命周期保持一致**。
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)。
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
它是唯一一个在Java虚拟机规范中没有规定任何**outofMemoryError**情况的区域。
### 作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
![image-20200705155728557](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/145024-405052.png)
### 代码演示
我们首先写一个简单的代码
```java
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
```
通过PC寄存器,我们就可以知道当前程序执行到哪一步了 ![image-20200705161007423](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/145109-366402.png)
### 使用PC寄存器存储字节码指令地址有什么用呢?
因为**CPU需要不停的切换各个线程**,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
![image-20200705161409533](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/145511-242300.png)
### PC寄存器为什么被设定为私有的?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
![image-20200705161812542](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/145530-542517.png)
### CPU时间片
CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
在宏观上:俄们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
![image-20200705161849557](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/145556-88561.png)
## 虚拟机栈
### 概述
#### 为什么会用虚拟机栈
由于跨平台的设计,java的指令都是通过栈来设计的,不同平台的cpu架构不同,所以不能设计为基于寄存器的,
虚拟机栈的优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令
#### 栈与堆
栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。
堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。
区别和联系:
1. 申请方式
堆是由程序员自己申请并指明大小,在c中malloc函数 如p1 = (char *)malloc(10);
栈由系统自动分配,如声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
2. 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会 遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内 存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大 小,系统会自动的将多余的那部分重新放入空闲链表中。
3. 申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结 构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4. 申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
#### 是什么(详细定义)
java virtual machine stack ,每个线程创建时都会创建一个虚拟机栈,其内部保存了很多栈帧(stack frame),对应着一次次的java方法调用,所以虚拟机栈是**线程私有的**
生命周期和线程一致,作用是主管java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用与返回。
#### 代码举例
```java
public void methodA(){
int i = 10;
int j = 20;
methodB();
}
public void methodB(){
int k = 30;
int m = 40;
}
```
![image-20210323190129651](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/141800-30055.png)
#### 栈的特点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器,
jvm直接对栈 的操作只有两个,
1. 每个方法执行,伴随着进栈(入栈、压栈)
2. 执行结束后的出栈工作
对于栈不存在垃圾回收问题,但是栈是存在oom(out of memory)
![image-20200705165025382](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/153242-857049.png)
#### 开发中遇到哪些异常?
栈中可能出现的异常
Java 虚拟机规范9允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量**超过Java虚拟机栈允许的最大容量**,Java虚拟机将会抛出一个StackoverflowError 异常。
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,**或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈**,那Java虚拟机将会抛出一个**outofMemoryError **异常。
```java
/**
* 默认设置 9525左右
* -Xss256k 增加编译选项 1938左右 设置栈的大小为256kb
* -Xss1024M 丧心病狂的这个竟然也可以
*/
public class StackErrorTest {
static Integer i = 0;
public static void main(String[] args) {
System.out.println(i++);
main(args);
}
}
```
#### 设置栈的内存大小
![image-20210326161254284](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/161254-657768.png)
#### 栈的存储单位
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的**每个方法**都各自对应**一个栈帧(Stack Frame)**。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
> OOP的基本概念:类和对象
>
> 类中基本结构:field(属性、字段、域)、method
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则
在一条活动线程中,**一个时间点上,只会有一个活动的栈帧**。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),**与当前栈帧相对应的方法就是当前方法(Current Method)**,定义这个方法的类就是当前类(Current Class)。
![image-20200705203142545](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/163125-6516.png)
```java
public class StackFrameTest {
public static void main(String[] args) {
method01();
}
private static int method01() {
System.out.println("方法1的开始");
int i = method02();
System.out.println("方法1的结束");
return i;
}
private static int method02() {
System.out.println("方法2的开始");
int i = method03();;
System.out.println("方法2的结束");
return i;
}
private static int method03() {
System.out.println("方法3的开始");
int i = 30;
System.out.println("方法3的结束");
return i;
}
}
/*
方法1的开始
方法2的开始
方法3的开始
方法3的结束
方法2的结束
方法1的结束
*/
```
#### 栈运行原理
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
**如果没有处理异常,则会一层层往上抛出异常。**
所有的方法都会有return,只不过返回类型为void可以省略
### 栈帧的内部结构
#### 概述
1. 局部变量表
2. 操作数栈/表达式栈
3. 动态链接 (指向运行时常量池的方法引用)
4. 方法返回信息 (方法正常退出或者异常退出的定义)
5. 一些附加信息
![image-20200705204836977](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/165303-172130.png)
#### 局部变量表
每一个栈帧都有的表,注意 加入static修饰的方法的局部变量表的参数个数比真正定义的要多一个,**因为需要多定义一个this**
- 局部变量表:Local Variables,被称之为局部变量数组或本地变量表
- 定义为一个数字数组(byte short char int boolean long和double都是数值),主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,**因此不存在数据安全问题**
- 局部变量表所需的**容量大小是在编译期确定下来的**,并保存在方法的Code属性的maximum local variables数据项中。在方法**运行期间是不会改变局部变量表的大小**的。
```java
package code;
import javax.swing.*;
import java.util.Date;
public class LocalVariableTable {
private int count = 0;
// stack=2, locals=3 (局部变量表有三个变量 分别是args test 和 num ), args_size=1
public static void main(String[] args) {
LocalVariableTable test = new LocalVariableTable();
int num = 10;
test.test1();
}
// stack=3, locals=4 (局部变量表有四个变量 分别是this date name和info ), args_size=1
public void test1() {
Date date = new Date();
String name = "aaa";
String info = test2(date,name);
System.out.println(date + name);
}
public String test2(Date dateP,String name2) {
dateP = null;
name2 = "bbbbbb";
double weight = 150;
char gender = '男';
return dateP + name2;
}
public void test3(){
count++;
}
public void test4(){
int a = 0;
{
int b = 0;
b = a +1;
}
// 变量c使用之间已经销毁的变量b所占用的空间
int c = a + 1;
}
}
```
```bash
> javap -v LocalVariableTable.class
Classfile /F:/project/github/unfinished/jvmLearn/code/out/production/code/code/LocalVariableTable.class
Last modified 2021年3月26日; size 1778 bytes
MD5 checksum 4b400430d5bba830d949321ce9403232
Compiled from "LocalVariableTable.java"
public class code.LocalVariableTable
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // code/LocalVariableTable
super_class: #16 // java/lang/Object
interfaces: 0, fields: 1, methods: 6, attributes: 3
Constant pool:
#1 = Methodref #16.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#53 // code/LocalVariableTable.count:I
#3 = Class #54 // code/LocalVariableTable
#4 = Methodref #3.#52 // code/LocalVariableTable."<init>":()V
#5 = Methodref #3.#55 // code/LocalVariableTable.test1:()V
#6 = Class #56 // java/util/Date
#7 = Methodref #6.#52 // java/util/Date."<init>":()V
#8 = String #57 // aaa
#9 = Methodref #3.#58 // code/LocalVariableTable.test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#10 = Fieldref #59.#60 // java/lang/System.out:Ljava/io/PrintStream;
#11 = InvokeDynamic #0:#64 // #0:makeConcatWithConstants:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#12 = Methodref #65.#66 // java/io/PrintStream.println:(Ljava/lang/String;)V
#13 = String #67 // bbbbbb
#14 = Double 150.0d
#16 = Class #68 // java/lang/Object
#17 = Utf8 count
#18 = Utf8 I
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 LocalVariableTable
#24 = Utf8 this
#25 = Utf8 Lcode/LocalVariableTable;
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 test
#31 = Utf8 num
#32 = Utf8 test1
#33 = Utf8 date
#34 = Utf8 Ljava/util/Date;
#35 = Utf8 name
#36 = Utf8 Ljava/lang/String;
#37 = Utf8 info
#38 = Utf8 test2
#39 = Utf8 (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#40 = Utf8 dateP
#41 = Utf8 name2
#42 = Utf8 weight
#43 = Utf8 D
#44 = Utf8 gender
#45 = Utf8 C
#46 = Utf8 test3
#47 = Utf8 test4
#48 = Utf8 b
#49 = Utf8 a
#50 = Utf8 SourceFile
#51 = Utf8 LocalVariableTable.java
#52 = NameAndType #19:#20 // "<init>":()V
#53 = NameAndType #17:#18 // count:I
#54 = Utf8 code/LocalVariableTable
#55 = NameAndType #32:#20 // test1:()V
#56 = Utf8 java/util/Date
#57 = Utf8 aaa
#58 = NameAndType #38:#39 // test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#59 = Class #69 // java/lang/System
#60 = NameAndType #70:#71 // out:Ljava/io/PrintStream;
#61 = Utf8 BootstrapMethods
#62 = MethodHandle 6:#72 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Lja
va/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#63 = String #73 // \u0001\u0001
#64 = NameAndType #74:#39 // makeConcatWithConstants:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
#65 = Class #75 // java/io/PrintStream
#66 = NameAndType #76:#77 // println:(Ljava/lang/String;)V
#67 = Utf8 bbbbbb
#68 = Utf8 java/lang/Object
#69 = Utf8 java/lang/System
#70 = Utf8 out
#71 = Utf8 Ljava/io/PrintStream;
#72 = Methodref #78.#79 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/Me
thodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#73 = Utf8 \u0001\u0001
#74 = Utf8 makeConcatWithConstants
#75 = Utf8 java/io/PrintStream
#76 = Utf8 println
#77 = Utf8 (Ljava/lang/String;)V
#78 = Class #80 // java/lang/invoke/StringConcatFactory
#79 = NameAndType #74:#84 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lan
g/Object;)Ljava/lang/invoke/CallSite;
#80 = Utf8 java/lang/invoke/StringConcatFactory
#81 = Class #86 // java/lang/invoke/MethodHandles$Lookup
#82 = Utf8 Lookup
#83 = Utf8 InnerClasses
#84 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#85 = Class #87 // java/lang/invoke/MethodHandles
#86 = Utf8 java/lang/invoke/MethodHandles$Lookup
#87 = Utf8 java/lang/invoke/MethodHandles
{
public code.LocalVariableTable();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field count:I
9: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcode/LocalVariableTable;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #3 // class code/LocalVariableTable
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: bipush 10
10: istore_2
11: aload_1
12: invokevirtual #5 // Method test1:()V
15: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 11
line 12: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
8 8 1 test Lcode/LocalVariableTable;
11 5 2 num I
public void test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: new #6 // class java/util/Date
3: dup
4: invokespecial #7 // Method java/util/Date."<init>":()V
7: astore_1
8: ldc #8 // String aaa
10: astore_2
11: aload_0
12: aload_1
13: aload_2
14: invokevirtual #9 // Method test2:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
17: astore_3
18: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_1
22: aload_2
23: invokedynamic #11, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
28: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 11
line 18: 18
line 20: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 this Lcode/LocalVariableTable;
8 24 1 date Ljava/util/Date;
11 21 2 name Ljava/lang/String;
18 14 3 info Ljava/lang/String;
public java.lang.String test2(java.util.Date, java.lang.String);
descriptor: (Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=6, args_size=3
0: aconst_null
1: astore_1
2: ldc #13 // String bbbbbb
4: astore_2
5: ldc2_w #14 // double 150.0d
8: dstore_3
9: sipush 30007
12: istore 5
14: aload_1
15: aload_2
16: invokedynamic #11, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
21: areturn
LineNumberTable:
line 23: 0
line 24: 2
line 25: 5
line 26: 9
line 27: 14
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcode/LocalVariableTable;
0 22 1 dateP Ljava/util/Date;
0 22 2 name2 Ljava/lang/String;
9 13 3 weight D
14 8 5 gender C
public void test3();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return
LineNumberTable:
line 30: 0
line 31: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcode/LocalVariableTable;
public void test4();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: iconst_1
6: iadd
7: istore_2
8: iload_1
9: iconst_1
10: iadd
11: istore_2
12: return
LineNumberTable:
line 33: 0
line 35: 2
line 36: 4
line 38: 8
line 39: 12
LocalVariableTable:
Start Length Slot Name Signature
4 4 2 b I
0 13 0 this Lcode/LocalVariableTable;
2 11 1 a I
12 1 2 b I
}
SourceFile: "LocalVariableTable.java"
InnerClasses:
public static final #82= #81 of #85; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #62 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang
/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#63 \u0001\u0001
```
##### 插件的解释(.class文件的含义)
###### 行号表
![image-20210326194723764](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/194750-669575.png)
###### 本地变量表
加入static修饰
![image-20210326192351470](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/192352-694903.png)
![image-20210326200933897](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/200935-810342.png)
**注意图片中说错了 static修饰的函数是没有this 这个局部变量 所以 静态函数中不能使用this这个变量**
##### 关于slot的理解
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,**最基本的存储单元是Slot(变量槽)**局部变量表中存放编译期可知的**各种基本数据类型(8种)**,引用类型(reference),returnAddress类型的变量。
> byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。
> long和double则占据两个slot。
JVM会为局部变量表中的**每一个Slot都分配一个访问索引**,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
如果需要访问局部变量表中一个64bit的局部变量值时,**只需要使用前一个索引**(比如访问下图中的double q变量的时候直接访问4即可)即可。(比如:访问long或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
![image-20200705212454445](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/170300-155533.png)
##### Slot的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
如图所示 变量c使用之间已经销毁的变量b所占用的空间
![image-20210326202222725](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/202224-617721.png)
##### 静态变量与局部变量的对比
变量的分类:
- 按数据类型分:基本数据类型、引用数据类型
- 按类中声明的位置分:成员变量(类变量,实例变量)、局部变量
- 类变量:linking的paper阶段,给类变量默认赋值,init阶段给类变量显示赋值,即静态代码块
- 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前必须进行显式赋值,不然编译不通过。
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
我们知道**类变量表有两次初始化**的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
类变量
1. linking的prepare阶段给类变量默认赋值
2. initial阶段给类变量显式赋值即静态代码块赋值
实例变量
1. 随着对象的创建,在对空间分配实例变量空间,并进行默认赋值
和类变量初始化不同的是,**局部变量表不存在系统初始化的过程**,**这意味着一旦定义了局部变量则必须人为的初始化**,否则无法使用。
局部变量
1. 在使用前必须进行显式赋值,否则编译不通过
```java
public void test5(){
int num;
//报错 System.out.println(num);
}
```
在栈帧中,与**性能调优**关系最为密切的部分就是前面提到的**局部变量表**。在方法执行时,虚拟机使用局部变量表完成方法的传递。
**局部变量表中的变量也是重要的垃圾回收根节点**,只要被局部变量表中直接或间接引用的对象都不会被回收。
#### 操作数栈
##### 概述
操作数栈:Operand Stack
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 **操作数栈**,也可以称之为 **表达式栈**(Expression Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
- 比如:执行复制、交换、求和等操作
![image-20200706090618332](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/203856-192029.png)
代码举例
![image-20200706090833697](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/205738-88898.png)
操作数栈,**主要用于保存计算过程的中间结果**,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。.
> 这个时候数组是有长度的,因为数组一旦创建,那么就是不可变的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。
栈中的任何一个元素都是可以任意的Java数据类型
- 32bit的类型占用一个栈单位深度
- 64bit的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。|
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
##### 代码追踪
我们给定代码
```java
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
```
使用javap 命令反编译class文件: javap -v 类名.class
执行流程如下所示:
首先执行第一条语句,PC寄存器指向的是0,也就是指令地址为0,然后使用bipush让操作数15入栈。
![image-20200706093131621](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211615-324590.png)
执行完后,让PC + 1,指向下一行代码,下一行代码就是将操作数栈的元素存储到局部变量表1的位置,我们可以看到局部变量表的已经增加了一个元素
![image-20200706093251302](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211633-631604.png)
> 为什么局部变量表不是从0开始的呢?
>
> 其实局部变量表也是从0开始的,但是因为0号位置存储的是this指针,所以说就直接省略了~
然后PC+1,指向的是下一行。让操作数8也入栈,同时执行store操作,存入局部变量表中
![image-20200706093646406](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211615-583787.png)
![image-20200706093751711](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211621-680288.png)
然后从局部变量表中,依次将数据放在操作数栈中
![image-20200706093859191](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211616-219554.png)
![image-20200706093921573](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211619-223169.png)
然后将操作数栈中的两个元素执行相加操作,并存储在局部变量表3的位置
![image-20200706094046782](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211645-634916.png)
![image-20200706094109629](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/211617-570276.png)
最后PC寄存器的位置指向10,也就是retur n方法,则直接退出方法
##### i++与++i区别(以后再说)
````java
public void plusPlus(){
int a1 = 10;
a1++;
int b1 = 10;
++b1;
int a2 = 10;
int b2 = a2++;
int a3 = 10;
int b3 = ++a3;
int a4 = 10;
a4 = a4++;
int a5 = 10;
a5 = ++a5;
int a6 = 10;
int a7 = a6++ + ++a6;
}
/////////////////////////////////////////////////////
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 bipush 10
8 istore_2
9 iinc 2 by 1
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
32 bipush 10
34 istore 7
36 iload 7
38 iinc 7 by 1
41 istore 7
43 bipush 10
45 istore 8
47 iinc 8 by 1
50 iload 8
52 istore 8
54 bipush 10
56 istore 9
58 iload 9
60 iinc 9 by 1
63 iinc 9 by 1
66 iload 9
68 iadd
69 istore 10
71 return
````
##### 栈顶缓存技术
栈顶缓存技术:Top Of Stack Cashing
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,**将栈顶元素全部缓存在物理CPU的寄存器中**,以此降低对内存的读/写次数,提升执行引擎的执行效率。
> 寄存器:指令更少,执行速度快
#### 动态链接
##### 概述
动态链接:Dynamic Linking
![image-20200706100311886](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/213450-260267.png)
> 动态链接、方法返回地址、附加信息 : 有些地方被称为帧数据区
每一个栈帧内部都包含一个指向**运行时常量池**中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么**动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。**
![image-20200706101251847](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/220237-912273.png)
> 为什么需要运行时常量池?
>
> 因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间
>
> 常量池的作用:就是为了提供一些符号和常量,便于指令的识别
##### javap -v 解析.class文件
![image-20210326221336245](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/221339-153718.png)
![image-20210326221424939](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/26/221425-497688.png)
##### 方法调用:解析与分配
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
###### 链接
1. 静态链接 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
2. 动态链接 如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
###### 绑定机制
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
1. 早期绑定 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
2. 晚期绑定 如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
###### 早晚期绑定的发展历史
随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
```java
interface Huntable{
void hunt();
}
class Animal{
void eat(){System.out.println("动物吃东西"); }
}
class Dog extends Animal implements Huntable{
@Override
void eat() {System.out.println("狗吃骨头");}
@Override
public void hunt() {System.out.println("我是猎狗 捕猎东西");}
}
class Cat extends Animal implements Huntable{
@Override
void eat() {System.out.println("猫吃鱼");}
@Override
public void hunt() {System.out.println("猫抓耗子");}
}
public class AnimalTest {
public void showAnimal(Animal animal){
// invokeVirtual
animal.eat(); // 晚期绑定
}
private void test(){
System.out.println("我是test");
}
public void showHunt(Huntable h){
// invokeInterface
h.hunt(); //晚期绑定
}
}
```
![image-20210508203030506](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202105/08/203031-442982.png)
###### 虚方法和非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
- 静态方法、**私有方法(java11中是invokeVirtual java8中是invokeSpecial)**、final方法(父类的final方法不一定是invokeSpecial 如果不是super.方法() 的话就是 invokeVirtual但是仍然被理解为非虚方法)、实例构造器、父类方法都是非虚方法。
- 其他方法称为虚方法。
> 子类对象的多态的使用前提
>
> - 类的继承关系
> - 方法的重写
```java
class Father{
static void showStatic(){ System.out.println("我是Father的showStatic"); }
public void showTest(){ System.out.println("我是Father的showTest"); }
public final void fatherShowFinal(){ System.out.println("我是Father的fatherShowFinal"); }
public void showAbc(){ System.out.println("abc"); }
}
public class Son extends Father{
static void showStatic() { System.out.println("我是Son的showStatic"); }
private void showPrivate() { System.out.println("我是Son的showPrivate"); }
public void sonShowPublic(){ System.out.println("我是Son的sonShowPublic"); }
void show(){
showStatic(); //invokestatic
super.showStatic(); //invokestatic
showPrivate(); //invokevirtual 加上this(this.showPrivate())也没用 注意这里java11更改了 java8 中是invokespecial
super.showTest(); //invokespecial
// 下边这两行其实一样 但是 字节码却不一样 这是个历史遗留问题
// 此方法虽然是父类的方法 fatherShowFinal 有final修饰 不能被子类重写 应该也是非虚方法
fatherShowFinal(); //invokevirtual
super.fatherShowFinal(); // invokespecial
// 子类没有重写过 showAbc 有可能重写 所以是 invokevirtual
showAbc(); //invokevirtual
}
}
```
![image-20210508214319951](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202105/08/214321-841669.png)
###### 方法调用指令
虚拟机中提供了以下几条方法调用指令:
1. 普通调用指令:
- invokestatic:调用静态方法,解析阶段确定唯一方法版本
- invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
2. 动态调用指令:
- invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(fina1修饰的除外)称为虚方法。
```java
interface Func{
public boolean func(String str);
}
public class LambdaTest {
public void lambdaTest(Func f){
return;
}
public static void main(String[] args) {
LambdaTest lambdaTest = new LambdaTest();
Func f = str -> { // 这一句调用了invokednamic 指令
return true;
};
lambdaTest.lambdaTest(f);
}
}
```
![image-20210508194935935](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202105/08/194937-380010.png)
JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现动态类型语言】支持而做的一种改进。
但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
###### 动态类型语言和静态类型语言
动态类型语言和静态类型语言两者的区别就在于**对类型的检查是在编译期还是在运行期**,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
> Java:String info = "mogu blog"; (Java是静态类型语言的,会先编译就进行类型检查)
>
> JS:var name = "shkstart"; var name = 10; (运行时才进行检查)
###### 方法重写的本质
1. Java 语言中方法重写的本质:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IlegalAccessError 异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.1ang.AbstractMethodsrror异常。
2. IllegalAccessError介绍
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
3. 方法的调用:**虚方法表**
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表
(virtual method table)(**非虚方法不会出现在表中**)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表是什么时候被创建的呢?
虚方法表会在**类加载的链接阶段**被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
![image-20200706144954070](F:/project/github/otherClone/LearningNotes/JVM/1_内存与垃圾回收篇/5_虚拟机栈/images/image-20200706144954070.png)
如上图所示:如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,**否则将会直接连接到Object的方法**中。
### 方法返回地址
存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,**在方法退出后都返回到该方法被调用的位置**。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过**异常表**来确定,**栈帧中一般不会保存这部分信息**。
当一个方法开始执行后,只有两种方式可以退出这个方法:
执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
- 一个方法在正常调用完成之后,**究竟需要使用哪一个返回指令**,还需要**根据方法返回值的实际数据类型而定**。
- 在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn(引用类型)。另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用。
```java
public class ReturnAddressTest {
// 默认构造器 返回 return
static {
int i = 0; //静态代码块返回 return
}
private void voidMethod(){
//return
}
private byte byteMethod(){
return 0; //ireturn
}
private short shortMethod(){
return 1; //ireturn
}
private char charMethod(){
return 'a'; //ireturn
}
private int intMethod(){
return 3; //ireturn
}
private Integer integerMethod(){
return 3; //areturn 引用类型返回
}
private long longMethod(){
return 4; // lreturn
}
private float floatMethod(){
return 5.0f; // freturn
}
private double doubleMethod(){
return 6.0; // dreturn
}
private String stringMethod(){
return "hello"; //areturn 引用类型返回
}
private Date dateMethod(){
return null; //areturn 引用类型返回
}
}
```
在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
![image-20200706154554604](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/27/160306-118850.png)
![image-20210508225319041](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202105/08/225320-489380.png)
```java
public class ReturnAddressTest {
public void method1(){
try {
method2();
} catch (Exception e) {
e.printStackTrace();
}
}
public void method2() throws Exception {
FileReader fileReader = new FileReader("a.txt");
char[] cBuffer = new char[1024];
int len;
while ((len = fileReader.read(cBuffer) )!= -1){
String str = new String(cBuffer,0,len);
System.out.println(str);
}
fileReader.close();
}
}
// method1 的javap -v解析内容
/*
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: invokevirtual #9 // Method method2:()V
4: goto 12
7: astore_1
8: aload_1
9: invokevirtual #11 // Method java/lang/Exception.printStackTrace:()V
12: return
Exception table: 异常表
from to target type
0 4 7 Class java/lang/Exception
*/
```
#### 总结
本质上,**方法的退出就是当前栈帧出栈的过程**。此时,**需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去**。
正常完成出口和异常完成出口的区别在于:**通过异常完成出口退出的不会给他的上层调用者产生任何的返回值**。
### 一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
### 栈的相关面试题
- 举例栈溢出的情况?(StackOverflowError)
- 通过 -Xss设置栈的大小
- 调整栈大小,就能保证不出现溢出么?
- 不能保证不溢出
- 分配的栈内存越大越好么?
- 不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
- 垃圾回收是否涉及到虚拟机栈?
- 不会
- 方法中定义的局部变量是否线程安全?
- 具体问题具体
- 总结一句话就是:如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
- 运行时数据区,是否存在Error和GC?
| 运行时数据区 | 是否存在Error | 是否存在GC |
| ------------ | ------------- | ---------- |
| 程序计数器 | 否 | 否 |
| 虚拟机栈 | 是 | 否 |
| 本地方法栈 | 是 | 否 |
| 方法区 | 是(OOM) | 是 |
| 堆 | 是 | 是 |
## 本地方法接口
### 什么是本地方法
简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "c" 告知c++编译器去调用一个c的函数。
"A native method is a Java method whose implementation is provided by non-java code."(本地方法是一个非Java的方法,它的具体实现是非Java代码的实现)
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
![image-20200706164139252](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/27/203003-891094.png)
代码举例说明Native方法是如何编写的
```java
public class IhaveNatives {
public native void Native1(int x);
native static public long Native2();
native synchronized private float Native3(Object o);
native void Natives(int[] ary) throws Exception;
}
```
> 需要注意的是:标识符native可以与其它java标识符连用,但是abstract除外
### 为什么使用Native Method?
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对**程序的效率**很在意时,问题就来了。
#### 与Java环境的交互
有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统交互,如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
#### 与操作系统的交互
JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
#### Sun's Java
**Sun的解释器是用C实现的**,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread的setpriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setpriorityo()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setpriority()ApI。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVw调用。
### 现状
**目前该方法使用的越来越少了**,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
## 本地方法栈
Java虚拟机栈于管理Java方法的调用,而**本地方法栈用于管理本地方法的调用**。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackoverflowError 异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个outofMemoryError异常。
本地方法是使用C语言实现的。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
![image-20200706174708418](http://tuchuang1234.oss-cn-shenzhen.aliyuncs.com/typora/202103/27/203545-314637.png)
当某个线程调用一个本地方法时,它就进入了一个全新的并且**不再受虚拟机限制的世界**。**它和虚拟机拥有同样的权限。**
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
- 它甚至可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存。
**并不是所有的JVM都支持本地方法**。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
在**Hotspot JVM**中,**直接将本地方法栈和虚拟机栈合二为一**。
jvm学习上