JVM内存结构


堆
堆的特点
- 线程共享,一块最大的内存存储区
- 通过new创建的对象,数组和字符常量池都在堆中
- 需要考虑线程安全问题,有垃圾回收机制
堆内存分配

堆内存有新生代和老年代之分
- 新生代:
新生代由伊甸园(Eden)和两个幸存者区(suervior space)组成
伊甸园用来存放新创建的对象
幸存者区是通过 from和to区不断交换来运行的,一次gc,幸存对象会从from到to,再一次gc,幸存对象又会从to到from - 老年代:新生代中经过多次gc仍然没有被回收的对象和太大的新生代区存不下的对象
晋升到老年代的方法
- 年龄阈值:当对象在 survivor 区存活了 15 次(默认)之后,会被移到老年代区。可以通过JVM参数
-XX:MaxTenuringThreshold 修改。 - 动态对象年龄判定:动态对象年龄判定:当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄。
- survior空间不足:当存活下来的对象大于survivor区容量的时候,会被移到老年代区。
示例:
假设新生代由100MB的Eden空间和两个50MB的Survivor空间组成,老年代有500MB的空间。
初始情况下,所有新创建的对象都分配在Eden空间。
进行第一次GC,此时Eden空间有80MB的对象,被GC后只有30MB的对象存活。这些存活的对象被移动到Survivor1,Eden被清空。
再次分配对象,Eden空间再次填满到80MB,此时Survivor1中还有30MB的存活对象。
进行第二次GC,Eden区的80MB对象中,60MB存活,加上Survivor1中的30MB存活对象,一共有90MB需要被移动到Survivor2,但Survivor2只有50MB的容量。
此时,JVM会检查Survivor1中对象的年龄,并将年龄大的对象提前晋升到老年代,假设10MB的对象被晋升,这样剩下20MB的对象与Eden区的60MB存活对象能够被移动到Survivor2。
如果Survivor空间依旧不足以处理这60MB的对象,那么无论年龄如何,都会将多出来的部分提前晋升到老年代。
GC的这些细节实际上取决于使用的垃圾收集器以及JVM的配置参数,不同的垃圾收集器(如Serial, Parallel, CMS, G1,
ZGC等)会以不同的方式管理这些区域。
堆内存检验方式
- jmp
jps 查看有哪些进程
jmp -heap[进程ID] 查看进程的堆内存 - jconsole
这个指令可以图形查看线程堆内存的变化过程 - jvisualvm
这是jvm的一个监控和调试程序
虚拟机栈
每个线程都有自己的虚拟机栈,这个栈用于存储栈帧。每当一个线程调用一个方法时,JVM就会为这个方法创建一个栈帧,并且将它压入虚拟机栈中。栈帧是用来存储局部变量、执行运算过程中的操作栈、动态链接信息以及方法返回地址等数据。

- 特点:
线程私有,每个线程运行所需要的内存,成为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 - 局部变量表
用来存储方法的参数和方法内部定义的局部变量。这些数据包括各种基本数据类型(int、float、long、double等)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。 - 操作栈
每个栈帧内部含有一个操作数栈,通常也叫做操作栈。这是一个后进先出(LIFO)的栈,用于执行方法中的字节码指令。操作数栈的主要作用是作为计算过程中的临时存储空间,用于存储操作指令的输入和输出参数。 - 动态链接
每个栈帧内部含有一个指向运行时常量池中该栈帧所属方法的引用,这使得当前方法能够动态链接到其它方法和变量。简而言之,动态连接是指方法在运行时实际引用的地址可以被替换成其他的方法或变量地址,这为Java的多态和方法重载提供了基础。 - 方法返回地址
当一个方法开始执行后,它需要知道在完成执行后返回到哪里。方法返回地址就是保存这个信息的地方,它指向调用该方法的位置的下一条指令地址。 - 栈内存溢出
栈帧过多导致栈内存溢出(如方法递归调用没有设置下线)
栈帧过大导致栈内存溢出栈内存自己会释放,所以不需要垃圾回收
程序计数器
- 特点:
是线程私有的(每个线程都有自己的程序计数器,因为每个线程执行地址不一样)
不会存在内存溢出(由jvm规定的) - 示例:
public class Example {
public static void main(String[] args) { // 1
int a = 5; // 2
int b = 10; // 3
int c = addNumbers(a, b); // 4
System.out.println(c); // 7
}public static int addNumbers(int a, int b) {
int result = a + b; // 5
return result; // 6
}
}在上面的方法中,程序计算器指向的地址分别是1到7,代码执行的每一步操作都会被记录
本地方法栈
本地方法栈的结构与虚拟机栈类似,也是由栈帧(Stack Frame)组成的,栈帧中保存了Native方法的局部变量、操作数栈、方法出口等信息。与虚拟机栈不同的是,本地方法栈中的方法不是用Java语言编写的,而是用其它语言编写的,比如C、C++等。因此,本地方法栈的结构与虚拟机栈类似,但是用于调用本地方法。
这里有一个简单的示例,演示了一个Java程序如何调用一个使用C语言编写的Native方法:
public class NativeExample {
static {
System.loadLibrary(“NativeLibrary”);
}
public native void nativeMethod();
public static void main(String[] args) {
NativeExample example = new NativeExample();
example.nativeMethod();
}
}
在这个示例中,NativeExample类中的nativeMethod方法是一个本地方法,它用native关键字修饰,表示这个方法是用其它语言实现的。在main方法中,通过example.nativeMethod()调用了这个本地方法。在执行时,虚拟机会使用本地方法栈来执行native Method方法的相关操作。
方法区
方法区实现方式:永久代、元空间
在早期的 Java 版本中,方法区与永久代有着密切的关系。方法区是一块用于存储类的相关信息、常量、静态变量、即时编译器优化后的代码等数据的内存区域。而永久代是 HotSpot 虚拟机中的概念,它实际上就是方法区的一种实现。
在 Java 7 及之前的版本中,永久代用于存储类和方法相关的信息,包括类的字节码、运行时常量池、字段、方法、构造函数等。由于永久代的大小在JVM启动时固定,并且随着应用的运行可能会出现永久代内存溢出的错误(OutOfMemoryError),在Java 8中被元空间所替代。
因此,从 Java 8 开始,永久代逐渐被元空间(Metaspace)所取代。它使用本地内存(即非JVM堆内存)来存储类元数据。这样的设计减少了内存溢出的可能性,因为元空间的大小仅受到系统可用内存的限制。当然,元空间中还是有一个初始大小,并且可以设置上限,一旦超过这个上限,仍然会抛出OutOfMemoryError异常。因此,方法区与永久代之间的关系在 Java 8 及以后的版本中已经不再存在。

元空间主要包括以下内容:
类的元数据信息:包括类的名称、方法名、访问修饰符、字段描述符等。
静态变量:类的静态变量存放在元空间中。
常量池:其中存放着字符串常量、字面量和符号引用。
方法区的对象不会被Java堆中的垃圾回收器以相同的方式回收,它有自己的内存管理系统(在使用元空间的情况下,内存可以从操作系统直接获取)。
让我们通过一段简单的Java代码,说明方法区中某些部分是如何被使用的:
public class ExampleClass {
// 常量池中的内容
private final static String CONSTANT_STRING = “Hello, World!”;
// 方法区中静态变量
private static int counter = 0;
// 类型信息和方法代码
public static void increment() {
counter++;
}
public static void main(String[] args) {
ExampleClass.increment();
System.out.println(CONSTANT_STRING);
}
}
在上述代码中:
字符串”Hello, World!”会被存储在常量池中。
静态变量counter会被存储在方法区。
类ExampleClass的类型信息(比如它的方法和字段)也会存储在方法区。
increment方法和main方法的代码,在被即时编译器编译之后,编译后的机器码也会存储在方法区。
当Java程序运行时,JVM会加载ExampleClass,这个过程中会将ExampleClass的类型信息、常量池中的常量、increment和main方法的字节码等数据存储在方法区,静态变量counter同样存储在方法区内,但具体是在永久代还是元空间则取决于JVM的版本及配置。在Java 8及之后版本,这部分数据会存储在操作系统的本地内存中,称作元空间。
方法栈和本地方法栈的区别
方法栈(Method stack):
方法栈存储的是 Java 方法的调用信息。每当一个方法被调用时,JVM都会在方法栈中分配一个栈帧(Stack Frame),用于存储该方法的调用信息。
方法栈中的栈帧会随着方法的调用和返回而动态地被创建和销毁,方法栈的栈帧也包括了方法的参数、局部变量以及用于返回的指令地址等信息。
本地方法栈(Native method stack):
本地方法栈则是用于执行本地(Native)方法的栈,即使用本地语言(如 C 或 C++)编写的方法。它与方法栈类似,但是用于执行本地方法。
本地方法栈也会为每个本地方法分配一个栈帧,用于存储本地方法的调用信息。
运行时常量值和字符串常量值的区别
- 存储位置:运行时常量池存储在方法区(元空间)中,而字符串常量池在 JDK 8 时存储在堆中。
作用:运行时常量池主要存储编译期间生成的字面量、符号引用等,而字符串常量池则用于存储字符串对象实例的引用。
动态性:运行时常量池在运行期间可以动态地放入新的常量,而字符串常量池则相对较为固定。
以下是一个具体的示例来说明它们的区别: - 假设有一个类 MyClass,其中包含一个字符串常量 STRING_CONSTANT。
在编译阶段,STRING_CONSTANT 会被存储在 class 文件的常量池中。当类加载器加载 MyClass 类时,常量池中的内容会被复制到运行时常量池中。 - 在运行时,如果创建了一个 MyClass 的实例,并调用了 STRING_CONSTANT,那么虚拟机首先会在运行时常量池中查找该字符串的引用。如果找到了,就直接使用该引用;如果没有找到,就会在字符串常量池中创建一个新的字符串对象,并将其引用存储在运行时常量池中。
总结
- 程序计数器:存储jvm指令的执行地址,不会内存溢出
- 虚拟机栈:每个线程运行时所需要的内存,每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存, 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 本地方法栈:存储非java代码编写的本地方法
- 堆:通过 new 关键字,创建对象都会使用堆内存。同时包含字符串常量池和数组。
- 方法区:它存储每个类的结构,如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法
- 共同特点:程序计数器、栈是线程私有;方法区、堆是线程共享。
程序计数器不会内存溢出,其他都会。
JVM常用调优参数
针对Java虚拟机(JVM)的性能调优,有一些常见的调优参数可以用来提高应用程序的性能和稳定性。以下是一些常用的JVM调优参数:
堆内存设置
-Xms:设置JVM初始堆大小
-Xmx:设置JVM最大堆大小
-Xmn:设置新生代大小
垃圾回收器选择
-XX:+UseSerialGC:使用串行垃圾回收器
-XX:+UseParallelGC:使用并行垃圾回收器
-XX:+UseConcMarkSweepGC:使用CMS垃圾回收器
-XX:+UseG1GC:使用G1垃圾回收器
垃圾回收相关参数
-XX:NewRatio:设置新生代与老年代的比例
-XX:SurvivorRatio:设置Eden区与Survivor区的比例
-XX:MaxTenuringThreshold:设置对象进入老年代的年龄阈值
元空间(Metaspace)设置
-XX:MaxMetaspaceSize:设置元空间最大大小
-XX:MetaspaceSize:设置元空间初始大小
并发标记
-XX:+UseConcMarkSweepGC:启用CMS垃圾回收器
-XX:+UseG1GC:启用G1垃圾回收器
线程堆栈大小
-Xss:设置每个线程的堆栈大小
性能监控参数
-XX:+HeapDumpOnOutOfMemoryError :OOM时自动打印堆转储文件
-XX:HeapDumpPath: 存储文件地址
-XX:+PrintGCDetails:输出GC的详细日志
-XX:+PrintGCTimeStamps:输出GC发生的时间戳


