大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 035 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。
–
运行时内存结构是 JVM 在执行 Java 程序时管理和分配内存资源的基础。不同的内存区域负责存储不同类型的数据,如堆内存用于对象实例,方法区用于类信息等。本篇文章将详细探讨 JVM 的运行时内存结构,帮助你理解内存管理的原理,以及如何避免常见的内存相关问题。
在编程世界里,内存管理一直是一个令人着迷的话题。在 C 语言中,内存的使用主要涉及到手动分配、使用和释放内存。这与 Java 的自动垃圾回收机制不同。C 语言通过指针直接操作内存,而 Java 隐藏了这些底层细节。
想象一下,你是一个雕刻家,手中拿着凿子和锤子,面对一块未经雕琢的大理石。在C语言中,内存管理就像这样的雕刻过程。你需要精确地决定从哪里切割、去除多少材料,每一下都要小心翼翼。
看看这个 C 语言的例子:
#include
#include
int main() {
int *ptr; // 这就像是指向你的大理石块的指针
int n = 5; // 你想雕刻的数字数量
ptr = (int*)malloc(n * sizeof(int)); // 你在大理石块上划出你要工作的区域
if (ptr == NULL) {
printf("Memory allocation failed.\n"); // 如果大理石块有问题,你就停止工作
exit(1);
}
for (int i = 0; i < n; i++) {
ptr[i] = i + 1; // 在每个区域雕刻一个数字
}
free(ptr); // 完成后,清理掉所有碎屑
return 0;
}
在这个例子中,我们使用 malloc
(内存分配)函数在内存中划出一块区域,就像在大理石上划出我们要雕刻的部分。然后我们在这块区域上工作,最后用 free
函数清理掉所有碎屑,释放这块区域。这需要精确的控制和细致的关注,一点小错误就可能导致整个作品毁坏。
现在,想象你是一个舞者,参加一场精心编排的舞蹈。在 Java 世界中,Jvm 像是你的舞伴,它自动地引导你,你只需要专注于舞蹈的步伐和节奏。
看看这个 Java 的例子:
public class Main {
public static void main(String[] args) {
int n = 5;
int[] arr = new int[n]; // Jvm 为你准备了舞台
for (int i = 0; i < arr.length; i++) {
arr[i] = i + 1; // 你在舞台上跳舞,每一步都被自动记录
}
// 舞蹈结束后,Jvm 会清理舞台,你无需担心
}
}
在这个例子中,当我们创建一个数组时,Jvm 自动在内存中为它分配空间。这就像是自动为你准备舞台。你只需要在这个空间上"跳舞"(编写代码)。当舞蹈结束,也就是当我们的数组不再需要时,Jvm 会自动进行"清理工作"(回收内存)。这个过程完全自动化,免去了你手动管理内存的麻烦。
通过这两个例子,我们看到了 C 和 Java 在内存管理上的根本不同。在 C 中,程序员必须像雕刻家一样精确地掌握每一块内存。而在 Java 中,Jvm 的垃圾回收机制就像是一位神奇的舞伴,它自动地引导着内存的分配和释放。这不仅简化了编程过程,也减少了错误的可能性,让程序员可以专注于创造性的工作。
这种从 C 到 Java 的转变,不仅是从手动到自动的变化,更是一种编程哲学的进化。在C中,你是内存的主宰,每一个决定都由你控制。而在 Java 中,你交出了部分控制,以换取更高的安全性和开发效率。Java 的自动内存管理减少了错误的可能性,使程序员可以更自由地探索更复杂的逻辑和架构。
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(Java SE 7版)》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:
这是 Jvm 中最大的一块内存区域。也是被所有线程共享的一块内存区。用于存储 Java 应用程序创建的对象实例和数组,堆是也在虚拟机启动时创建的,并且是垃圾收集器管理的主要区域,这里的对象不需要手动释放。
Java 堆在存储内容的构成上主要有:
对象实例:几乎所有通过 new
关键字创建的对象都在堆上分配。这包括用户自定义类的对象以及内置类的对象实例(如 Object
、Integer
、String
等)。
数组:无论是基本类型数组还是对象数组,都在堆上分配。
类实例变量:对象的非静态字段(无论是基本类型还是对象引用)随对象实例一起存储在堆中。
字符串常量池:从 Java 7 开始,字符串常量池被存储在堆中。包括所有的字符串字面量和通过 String.intern()
方法显式添加到池中的字符串实例。字符串常量池帮助节省内存,因为它存储了唯一的字符串实例,当创建已存在于池中的字符串时,直接返回池中的引用。
Jvm 内部结构:包括但不限于,某些 Jvm 内部维护的结构,如方法调用时的参数、返回值等。
垃圾收集根节点:活跃的线程、静态字段引用的对象、JNI 引用等,这些作为 GC Roots,垃圾收集器会从这些根节点开始搜索,以确定哪些对象是可达的,哪些是垃圾。
其他可能的结构或对象:根据不同 Jvm 的实现细节,可能还会有其他类型的数据结构或对象存储在堆上。
总结来说,Java 堆是存储 Java 应用程序创建的大部分动态数据的地方。这不仅包括各种类型的对象和数组,还包括特殊的结构如字符串常量池。堆的管理和垃圾回收机制对于 Java 应用程序的性能和稳定性至关重要。
此外 Java 堆也是垃圾回收的主要场所。因此很多时候也被称做 GC 堆(Garbage Collected Heap)。
从内存回收的角度来看 Jvm 的内存分区:
方法区(Method Area)也常常被称为永久代(Permanent Generation,简称 PermGen),但实际上是两个不同的概念,在 Java 的不同版本中扮演不同的角色。
总结来说,方法区是一个抽象概念,而永久代是这个概念在某些 Jvm 版本中的具体实现。从 Java 8 开始,永久代被元空间所取代,这是一种更灵活的内存管理方式。
以 JDK 8 之前的 HotSpot 实现中的方法区举例,它的主要内容有:
Java运行时常量池(Runtime Constant Pool)是Java虚拟机在运行时存储常量的一块区域。它是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
Java运行时常量池的主要作用有以下几个方面:
存储字面量:Java中的字符串、整数、浮点数等字面量都会被存储在运行时常量池中。这些字面量在编译期就确定了,存储在常量池中可以提高运行效率。
存储符号引用:Java中的类、方法、字段等符号引用也会被存储在运行时常量池中。符号引用是一种直接或间接指向目标的引用,存储在常量池中可以方便解析和使用。
动态生成的常量:在运行时,Java虚拟机还可以动态生成一些常量,并将其存储在运行时常量池中。例如通过String类的intern()方法将字符串对象加入常量池。
需要注意的是,运行时常量池是每个类的独立部分,它在类加载时被创建并初始化。不同类的常量池是相互独立的,即使两个类的常量内容相同,它们在运行时常量池中的地址也是不同的。
运行时常量池的存在可以提高Java程序的性能和效率,减少了重复的常量创建和存储。同时,它也为Java提供了一些特性,如字符串常量池、类的符号引用等。
在Java虚拟机中,符号引用(Symbolic Reference)是一种用来表示对类、方法、字段等符号的引用的数据类型。它是一种字面量形式的引用,与直接引用(Direct Reference)相对。
符号引用包含了以下信息:
类的全限定名(Fully Qualified Name) | 用于唯一标识一个类 | com.example.MyClass |
方法的名称和描述符 | 用于唯一标识一个方法,包括方法名和参数类型列表 | doSomething(int, String) |
字段的名称和描述符 | 用于唯一标识一个字段,包括字段名和字段类型 | count:int |
符号引用是在编译期生成的,它是一种与具体内存地址无关的引用。在虚拟机执行时,需要将符号引用解析为直接引用,即将符号引用转化为内存中的实际指针或偏移量。
通过符号引用,虚拟机可以在运行时动态地加载、解析和初始化类,以及调用类的方法和访问类的字段。符号引用的解析过程是虚拟机对类的动态链接的一部分。
需要注意的是,符号引用是在编译期确定的,而直接引用是在运行时确定的。符号引用是一种抽象的引用,而直接引用是具体的内存地址或偏移量。虚拟机在执行时会将符号引用解析为直接引用,以便进行具体的操作。
总的来说,符号引用是一种用于表示对类、方法、字段等符号的引用的数据类型,它包含了类的全限定名、方法的名称和描述符、字段的名称和描述符等信息。虚拟机在执行时会将符号引用解析为直接引用,以便进行具体的操作。
Java 虚拟机栈(Java Virtual Machine Stacks)是专门用于存储每个线程运行时的栈帧。每个 Java 线程在创建时都会创建自己的 Jvm 栈。这些栈在 Jvm 规范中被描述为线程私有的内存区域。
栈帧(Stack Frame):栈帧是 Jvm 栈的基本单位,每个栈帧对应着一个方法调用:
虚拟机栈的入栈和出栈操作主要与方法的调用和返回有关。这些操作围绕栈帧(Stack Frame)进行,每个栈帧代表一个方法的调用环境。
以下是虚拟机栈的入栈和出栈过程的详细解释:
入栈(Push):当一个方法被调用时,一个新的栈帧被创建并入栈。这个过程包括以下步骤:
this
引用被存储在局部变量表的第一个位置。方法的参数按顺序被存储在局部变量表中。出栈(Pop):当一个方法完成执行后,它的栈帧需要从虚拟机栈中出栈。这个过程包括:
此外虚拟机栈存在着两种主要的异常情况;
StackOverflowError
);如果方法调用的深度过大,超过了 Jvm 栈的最大深度,会发生栈溢出。OutOfMemoryError
):如果 Jvm 栈无法获得足够的内存,可能会发生内存不足错误。本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpo虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
直接内存(Direct Memory)在 Java 虚拟机(Jvm)的上下文中指的是一块不是由 Jvm 直接管理的内存区域,其主要通过 Native 方法直接分配在操作系统的物理内存上。以下是直接内存的一些重要特点和用途:
特点:
-Xmx
)的限制。用途:
可以使用 ByteBuffer.allocateDirect(int capacity)
方法来创建一个直接缓冲区,这将在直接内存中分配空间。以下是一个示例:
import java.nio.ByteBuffer;
public class DirectMemoryCache {
public static void main(String[] args) {
// 分配直接内存
int capacity = 1024 * 1024 * 10; // 10MB
ByteBuffer directBuffer = ByteBuffer.allocateDirect(capacity);
// 使用直接内存缓存
// 例如,写入数据
directBuffer.put((byte) 123);
// 在后续操作中读取数据
directBuffer.flip(); // 切换为读模式
byte b = directBuffer.get();
// 清理缓存以待再次使用
directBuffer.clear();
}
}
注意事项:
OutOfMemoryError
或者系统的内存溢出。总结:直接内存提供了一种在 Java 中使用高效内存的方式,特别适合于需要快速、大量 I/O 操作的应用程序。然而,它的使用需要仔细考量,以避免内存泄漏和其他内存管理问题。直接内存的使用是一种权衡,需要在性能提升和编程复杂度、资源管理之间找到平衡点。
Java 运行时内存结构可以通过多种 Jvm(Java 虚拟机)参数进行配置和优化。这些参数允许开发者调整内存大小、控制垃圾回收行为等,以适应不同的应用需求和提升性能。以下是一些常用的 Jvm 内存相关参数:
-Xms
:设置 JVM 启动时堆的初始内存大小。例如,-Xms256m
设置初始堆大小为 256MB。-Xmx
:设置 JVM 可以使用的最大堆内存大小。例如,-Xmx1024m
设置最大堆大小为 1024MB。-Xmn
:设置年轻代的大小。这个值可以影响老年代的大小,因为整个堆大小是固定的。-XX:MetaspaceSize
:设置元空间的初始大小。例如,-XX:MetaspaceSize=128m
设置元空间的初始大小为 128MB。-XX:MaxMetaspaceSize
:设置元空间的最大大小。默认情况下,元空间大小不限制。-XX:+UseG1GC
-XX:+UseParallelGC
等:选择使用特定的垃圾回收器。不同的垃圾回收器对内存管理策略有不同的影响。-XX:MaxDirectMemorySize
:设置直接内存的最大大小。这对于使用 NIO 进行 I/O 操作的应用程序特别重要。-XX:MinHeapFreeRatio
和 -XX:MaxHeapFreeRatio
:设置堆空闲空间的最小和最大百分比。JVM 将在这些阈值内动态调整堆的大小。-verbose:gc
, -XX:+PrintGCDetails
, -XX:+PrintGCTimeStamps
等:启用和配置垃圾回收日志,以监控和调试垃圾收集行为。注意事项:这些参数会根据不同的 Jvm 实现(如 HotSpot、OpenJ9)和版本有所不同。使用这些参数时,需要根据应用程序的具体需求和资源限制进行调整和优化。
调整 Jvm 参数是一个需要考虑多方面因素的过程,需要根据应用程序的行为、垃圾回收日志、性能指标等信息进行综合分析和调优。