1. Java语言的运行过程
首先,我们先了解一下Java语言的运行过程: 首先 .java文件 (源代码)经过 javac 编译后形成 .class文件 (字节码),然后通过 类加载器 加载到内存中初始化,接着在 JVM (Java虚拟机)中执行,最后通过 OS 操作 CPU 执行。
2. 什么是JVM?
相信大家都听过JDK 、JRE 和JVM,那他们之间有什么区别呢?
-
JVM
:Java Virtual Machine,Java虚拟机,是java程序的运行环境。真正运行Java程序的地方。 -
JRE
:Java Runtime Environment,Java运行时环境,包含了运行Java程序所需的一切,包括JVM和Java类库(依赖库)。不包含开发工具。 -
JDK
:Java Development Kit,java开发工具包。包含了完整的JRE,还包含了用于java开发的一些工具,如javac等,一般这些工具在JDK安装目录的bin目录下。JRE:包含JVM、核心类库、运行工具。
JDK:包含JVM、核心类库、开发工具。(JRE + JVM)
平时使用安装JDK即可。
JVM主要由四大部分组成:
ClassLoader
:类加载器Runtime Data Area
:运行时数据区Execution Engine
:执行引擎Native Interface
:本地方法接口
常见JVM的实现:Hotspot、J9、Zing、毕昇
3. JVM的特性
3.1 跨平台性
为什么Java代码可以一次编写、到处运行(跨平台),并且效果一致?
JVM是其关键。JVM本身是一种规范,只识别.class文件(字节码规范),JVM负责将字节码文件翻译成特定的机器码。同一份Java源代码无论在什么平台上运行,它只需要编译一次形成.class文件。只要在不同平台(Windows、Linux、Android等)上安装对应的JVM,就可以运行该字节码文件,与语言语法毫无关系。
JVM从软件层面屏蔽了不同操作系统在底层的硬件和指令的不同。
3.2 语言无关性
JVM并不关心.class文件由谁编译而来,本身是一种规范,也就是说,无论使用什么语言(如java、kotlin、groovy等),最后能编译出来class文件,就可以在JVM上运行,JVM存在语言无关性,所以java生态圈很强大。
4. JVM内存模型
下图为JVM内存结构模型:
graph LR 类加载器 --> 字节码解释器 --> 执行引擎 类加载器 --> JIT编译器 --> 执行引擎两种执行方式:
- 解释执行:JVM是由C++语言编写的,其中有C++解释器,负责先将Java语言解释翻译为C++语言。缺点是经过一次JVM翻译,速度慢一点。
- JIT执行 :JIT编译器在程序运行时,会将频繁执行的热点代码的字节码编译为本地机器代码,并进行优化,然后把编译后的机器码缓存起来,以备下次使用,从而提高了程序的执行效率。但编译时间较长。
5. 运行时数据区
运行时数据区:java虚拟机在执行java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。
- Java虚拟机栈(栈区)
- 本地方法栈
- Java堆(堆区)
- 方法区
- 程序计数器
直接内存:堆外内存,不是运行时数据区的一部分,但会被频繁使用。没有经过虚拟化。
5.1 程序计数器
-
作用:程序计数器就是行号指示器,是一块较小的内存空间,指向当前线程正在执行的字节码指令的地址。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
-
线程是否共享:线程私有。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程 之间计数器互不影响,独立存储
如果正在执行的是本地方法,这个计数器值则应为空。
5.2 java虚拟机栈
-
作用:存储当前线程运行Java方法所需的数据、指令、返回地址。
-
线程是否共享:线程私有。
启动一个线程创建一个虚拟机栈。虚拟机栈是线程私有的,栈的生命周期和线程是一样的。遵循先进后出原则。
-
栈帧:在每个Java方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成相应的调用,则出栈。
-
栈帧结构:
-
局部变量表:用于存储方法参数和局部变量的值。存放了编译期可知的基本数据类型、对象引用、returnAddress类型。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小(局部变量槽(Slot)的数量)。
-
操作数栈:用于存储计算过程中的操作数和中间结果。
-
动态链接:将符号引用转换为直接引用的过程。
-
方法出口:方法出口指向的是调用方法的指令地址,即调用方法的当前执行位置。当一个方法正常结束时,程序计数器(PC)将恢复到该地址,使得程序能够继续执行。
java
public class Test {
public static void main(String[] args) {
A();
}
private static void A() {
B();
}
private static void B() {
C();
}
private static void C() {
}
}
-
栈大小 :虚拟机栈有大小限制。参数-Xss。默认值取决于平台。
应用场景:系统内存吃紧情况下。
系统剩下100M内存,一个线程堆栈大小默认1M,现在有200个线程分配内存。一般线程请求栈深度不会特别深,这时可以缩小栈大小-Xss256k。
-
可能产生的异常:
- StackOverError异常:线程请求栈深度 > 虚拟机所允许深度
- OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存
一个方法对应一个栈帧:
java
public class Student{
public static void main(String[] args) throws Exception{
Student student = new Student();
student.study();
}
public int study() throws Exception{
int a = 1;
int b = 2;
int c = (a + b) * 3;
return c;
}
}
以study()方法为例, 首先使用javap -c
命令查看study()方法的字节码:
最左侧0~12代表字节码地址,字节码偏移量,程序计数器中会记录。程序运行时程序计数器会记录着运行方法字节码的行号。
当执行ireturn
指令时,JVM会将当前方法的返回值(9)推送到调用方法的操作数栈中。
这里以int a = 1;
为例,剩下大家可以结合 详细规范 自己进行分析。
5.3 本地方法栈
- 作用:本地方法栈与虚拟机栈类似,主要区别就是本地方法栈是用于存储本地方法(native)的内存区域。
java
public native int hashCode();
- 可能产生的异常 :
- StackOverError异常:线程请求栈深度 > 虚拟机所允许深度
- OutOfMemoryError异常:当栈扩展时无法申请到足够的内存
JVM里会有默认参数设置。
通过jps找到运行进程:
然后通过jinfo -flags 3104找到具体参数信息:
然后通过这些信息: JVM申请内存
5.4 Java堆
-
作用:唯一目的存放Java对象实例。
-
线程是否共享:线程共享。
所有线程共享的一块区域,在虚拟机启动时创建。
-
大小:Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩 展来实现的(通过参数-Xmx和-Xms设定)。
-
特点:
- JVM中内存最大
- 是垃圾回收器管理的主要区域,也称GC堆。
-
可能产生的异常:
- OutOfMemoryError异常:堆中没有足够的内存完成对象实例的分配,堆无法再扩展。
5.5 方法区
- 作用:用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。别名叫作"非堆"(Non-Heap)。
- 线程是否共享:线程共享。
- 可能产生的异常 :
- OutOfMemoryError异常:方法区无法满足内存分配需求。
- 总结:该区域的内存回收主要是针对运行时常量池和对类的卸载。又称永久代:回收条件苛刻、很少出现垃圾收集。
5.6 运行时常量池
- 作用:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 可能产生的异常 :
- OutOfMemoryError异常:常量池无法满足内存分配需求。