大家在平时的开发过程中是否遇到过StackOverflowError
、OutOfMemoryError
等类似的内存溢出错误呢?大家又是怎么解决这个问题的?再来,大家在面试过程中有没有被面试官提问过jvm
的内部构造及如何优化的夺命连环call
呢?今天就让我们来一探究竟,先从jvm
的内部构造及原理说起,一步一步带大家解决jvm
的优化问题。
虚拟机简介
虚拟机(Virtual Machine
,简称VM
)就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
- 大名鼎鼎的
visual box
、vmware
就属于系统虚拟机,他们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。 - 程序虚拟机的代表就是
java
虚拟机(jvm
),他专门为执行单个计算机程序而设计,在java
虚拟机中执行的指令我们称为java
字节码指令。
无论是系统虚拟机还是程序虚拟机,在上边运行的软件都被限制于虚拟机提供的资源中。虚拟机所在的位置:硬件的操作系统之上。虚拟机与JDK
和JRE
的关系如图所示:
架构模型
Java
编译器输入的指令流基本上是一种基于栈的指令集架构,另一种指令集架构则是基于寄存器的指令集架构。那么他们之间有什么区别呢?
栈式架构特点:
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题,使用零地址指令方式分配;
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现;
- 不需要硬件支持,可移植性好,更好实现跨平台。
寄存器架构特点:
- 典型的应用是x86的二进制指令集:比如传统的
PC
以及Android
的Davlik
虚拟机; - 指令集架构则完全依赖于硬件,可移植性差;
- 性能优秀和执行更高效;
- 花费更小的指令去完成一项操作;
- 基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主。
机器指令是机器语言的一条语句,是一组有意义的二进制代码,一条机器指令通常分为两个部分:操作码和地址码。操作码指出该指令应该执行什么样的操作,代表了该指令的功能。地址码指出该指令操作的对象,给出被操作对象的地址。零地址指令指机器指令中操作数地址的个数为0,一地址指令指机器指令中操作数地址的个数为1,以此类推。
由于跨平台性的设计,java的指令都是根据栈来设计的,不同平台的cpu架构不同,所以不能设计为基于寄存器的。
举例:同样执行2+3的逻辑操作,其指令分别如下:
基于栈的计算流程(以Java虚拟机为例--idea中控制台使用javap -v XXX.class
执行)
0: iconst_2 //常量2入栈
1: istore_1 //将2从操作数栈存储到局部变量表 第1个位置
2: iconst_3 //常量3入栈
3: istore_2 //将2从操作数栈存储到局部变量表 第2个位置
4: iload_1 //位置为1的数据压入操作数栈
5: iload_2 //位置为2的数据压入操作数栈
6: iadd //常量2,3出栈,执行相加,并将结果压入操作数栈顶
7: istore_3 //结果5存到局部变量表 第三个位置
8: return
基于寄存器的计算流程:
mov eax,2 //将eax寄存器的值设为1
add eax,3 //使eax寄存器的值加3
java虚拟机的生命周期
虚拟机的启动:Java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的。
虚拟机的执行:虚拟机的任务是执行java程序,其真正执行的是一个叫做java虚拟机的进程。
虚拟机的退出:
- 程序正常执行结束;
- 程序在执行过程中遇到了异常或者错误而异常终止;
- 操作系统出现错误而导致java虚拟机进程终止;
- 线程调用Runtime类或者System类的exit方法,或者Runtime类的halt方法,并且java安全管理器也允许这次exit或halt操作;
- JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载java虚拟机时,java虚拟机的退出情况。
常见的JVM
如果说java
是跨平台的语言,那jvm
就是跨语言的平台。只要是将该语言的文件遵循jvm
的规范编译成jvm
可以识别的字节码文件,就可以在jvm
上运行。jvm
的特点:一次编译,到处运行;自动内存管理;自动垃圾回收功能。
HotSpot、JRockit与J9并称三大主流JVM:
HotSpot VM
:从JDK1.3
开始使用,到现在OpenJDK
中也在使用。采用解释器与即时编译器并存的架构,拥有成熟的热点代码探测技术和GC
机制。所谓热点探测技术有以下两个方面的体现:
一、通过计数器找到最具编译价值的代码,触发即时编译或者栈上替换功能--机器指令(cpu
可以直接执行的指令)本地缓存;
二、即时编译器和解释器协同工作,在最优化的程序响应时间与最佳执行性能之间平衡。
前端编译器(
javac
或者Eclipse JDT
中的增量式编译器)把Java
代码编译成字节码,字节码是可以发送给任何平台并且能在那个平台上运行的独立于平台的代码。
即时编译器(
JIT compiler,just-in-timecompiler
)是一个把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器(processor
)的指令的程序。
JRockit VM
:最初属于BEA
公司,2008年被Oracle
收购。它专注于服务器端应用,所以不太关注程序的启动速度,里边不包含解析器,号称是世界上最快的JVM
。它提供的Mission Control
服务套件,是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。它包括三个独立的应用程序:内存泄漏监测器(Memory Leak Detector
)、JVM运行时分析器(Runtime Analyzer
)和管理控制台(Management Console
)。
J9 VM
:J9
是IBM
开发的一个高度模块化的JVM
,在许多平台上,IBM J9 VM
都只能跟IBM
产品一起使用。2017年IBM
发布开源的OpenJ9
,并贡献给 Eclipse
基金会。
非主流JVM
介绍:
Azul VM
: 是Azul system
公司在Hot Spot
基础上进行的改进,是运行在其公司专有的硬件上,一个Azul VM
实例,都可以管理数十个CPU
以及数百G的内存资源,而且通过巨大内存范围内,实现可控的GC事件以及垃圾回收。
Graal VM
: 是一个高性能的通用虚拟机,可以运行使用JavaScript
,Python 3
,Ruby
,R
,基于JVM
的语言以及基于LLVM
的语言开发的应用。GraalVM
消除了编程语言之间的隔离性,并且通过共享运行时增强了他们的互操作性。它可以独立运行,也可以运行在OpenJDK
,Node.js
,Oracle
,MySQL
等环境中。它的口号“Run Programs Faster Anywhere”。
HotSpot的整体架构图
如图所示为HotSpot的架构简图,接下来我们会按照该图的执行顺序说一下JVM
里边的具体细节,如果你感兴趣的话,可以关注“阿Q说代码”!你也可以后台留言说出你的疑惑,阿Q将会在后期的文章中为你解答。