概述
Java虚拟机(JVM)是一种提供运行环境来驱动Java应用程序的引擎。它将Java字节码转换为机器语言,使Java程序能够在任何具有兼容JVM的设备或操作系统上运行。这一过程被称为Java的“编写一次,到处运行”的能力。
JVM运行时数据区
1.程序计数器寄存器(PC寄存器):包含当前正在执行的Java虚拟机指令的地址。
概述
用于存储当前正在执行的字节码指令的地址,确保字节码能够被正确地解释和执行。
作
-
指令跟踪
:
- 程序计数器保存当前线程正在执行的字节码指令的地址。
- 在JVM执行指令时,程序计数器的值会根据指令的执行情况进行更新,以指向下一条将要执行的指令。
-
多线程支持
:
- 在JVM中,每个线程都有独立的程序计数器。
- 这是因为每个线程都需要独立地执行字节码指令,独立的程序计数器保证了线程之间的执行互不干扰。
-
方法调用与返回
:
- 当方法调用时,程序计数器会保存调用方法的返回地址。
- 方法返回后,程序计数器会恢复到调用方法的地址,继续执行后续指令。
程序计数器是java虚拟机规范中唯一一个不存在内存溢出的区。
2.虚拟机栈:存储帧,保存局部变量和部分结果,并在方法调用和返回时发挥作用。
2.1概述
负责管理方法调用和执行。每个线程在JVM中都有自己的JVM栈,它随着线程的创建而创建,随着线程的结束而销毁。JVM栈中的每个元素被称为“栈帧”。
2.2栈的运行
方法调用和执行过程:
程序计数器恢复到调用方法的指令位置,继续执行。
-
方法调用:
- 当一个方法被调用时,JVM创建一个新的栈帧(Stack Frame),并将其推入当前线程的JVM栈顶。
- 栈帧中包含该方法所需的局部变量表、操作数栈、动态链接、方法返回地址和附加信息。
-
局部变量表的初始化:
- 局部变量表初始化并存储方法的参数和局部变量。
- 例如,对于方法
void foo(int a, int b)
,局部变量表中会有两个条目用于存储参数a
和b
。
-
操作数栈的使用:
- 操作数栈用于执行字节码指令,存储临时计算结果和操作数。
- 例如,字节码指令
iadd
从操作数栈中弹出两个整数,将它们相加,并将结果推回栈中。
-
动态链接:
- 动态链接部分包含指向运行时常量池的方法和字段引用。
- 在方法调用过程中,字节码指令会通过动态链接解析实际的内存地址。
-
方法执行:
- JVM解释或编译方法字节码并在栈帧中执行。
- 栈帧中的操作数栈和局部变量表在执行过程中不断变化,以反映当前的计算状态。
-
方法返回
:
- 方法执行完毕后,栈帧从栈顶弹出。
- 返回值(如果有)会被推入调用者的操作数栈中。
2.3栈帧(Stack Frame)的组成部分:
-
局部变量表(Local Variable Array)
:
- 存储方法参数和局部变量。
- 局部变量表的大小在编译时确定,并且在方法执行期间保持不变。
- 包含基本数据类型(int、float等)、对象引用和returnAddress类型(指向操作数栈的某个字节码指令地址)。
-
操作数栈(Operand Stack)
:
- 用于存储中间计算结果和操作数。
- 通过字节码指令执行入栈(push)和出栈(pop)操作。
- 大小在编译时确定。
-
动态链接(Dynamic Linking)
:
- 包含指向运行时常量池的方法引用。
- 支持方法调用过程中的动态链接,使得方法调用可以在运行时绑定到实际的内存地址。
-
方法返回地址(Return Address)
:
- 存储调用方法返回后,程序计数器需要继续执行的字节码指令地址。
-
附加信息
:
- 可能包含一些实现相关的信息,如调试信息。
3.本地方法栈:包含应用程序中使用的所有本地方法信息。
主要特点和作用:
- 线程私有:每个线程独立拥有自己的本地方法栈。
- 调用本地方法:存储调用本地方法时所需的信息,包括本地变量、参数和返回地址。
- 执行环境:为本地方法提供运行时环境,管理内存和上下文切换。
- 与Java方法栈的交互:通过Java本地接口(JNI)调用本地方法,并在本地方法中操作Java对象和方法。
4.堆:运行时数据区,从中为所有类实例和数组分配内存。
特点
- 全局共享:堆是JVM中的全局共享区域,所有线程都可以访问堆中的对象。
- 自动内存管理:通过垃圾回收机制自动管理内存,回收不再被引用的对象。
- 动态内存分配:支持动态分配和释放内存,根据程序的运行时需求进行调整。
- 分代收集:通常分为年轻代和老年代,优化垃圾回收性能。年轻代用于存储短生命周期的对象,老年代用于存储长生命周期的对象。
- 线程不安全:堆本身是线程不安全的,对堆进行操作时需要进行同步控制。
内存泄漏(Memory Leak)
内存泄漏是指程序中存在未被释放的对象引用,导致这些对象无法被垃圾回收器回收。内存泄漏会导致堆内存不断增加,最终可能导致堆内存溢出。
- 典型例子:长期持有不再使用的对象引用,如静态集合类(例如
HashMap
)中不断添加元素,且未清理过期元素。
堆内存诊断
- jps工具
- 查看当前系统中有哪些Java进程
- jmap工具
- 查看堆内存占用情况 jmap-heap 进程id
- jconsole工具
- 图形界面的,多功能检测工具,可以连续监测
5.方法区:存储类结构,如元数据、运行时常量池和方法代码。
5.1JDK8和之前的区别
- JDK 7:使用**永久代(PermGen)**来实现方法区。永久代用于存储类的元数据、常量池、静态变量等。永久代的大小是固定的,并且可能导致
OutOfMemoryError: PermGen space
错误。
- JDK 8:引入了**元空间(Metaspace)**来替代永久代。元空间将类的元数据存储在本地内存中,而不是堆内存,这使得类的元数据不再受到堆内存大小的限制。JDK 8 中的元空间动态调整其大小,通过
-XX:MaxMetaspaceSize
设置最大内存大小。
5.2常量池
-
类文件常量池:类文件常量池位于每个类文件的开头部分,是编译器生成的,包含了类或接口中使用的各种常量,包括字面量(字符串、整数、浮点数等)和符号引用(类名、字段名、方法名等)。
-
运行时常量池:运行时常量池是方法区的一部分,是类文件常量池在运行时的表示。每个类加载时,其类文件常量池会被载入到运行时常量池中。
-
类文件常量池:类文件常量池位于每个类文件的开头部分,是编译器生成的,包含了类或接口中使用的各种常量,包括字面量(字符串、整数、浮点数等)和符号引用(类名、字段名、方法名等)。
-
运行时常量池:运行时常量池是方法区的一部分,是类文件常量池在运行时的表示。每个类加载时,其类文件常量池会被载入到运行时常量池中。
(运行时常量池不仅可以存储编译期的常量,还可以在运行期间将新的常量放入池中,如通过String.intern()
方法将字符串添加到常量池。)