我们现在转向JVM的另一个关键领域:内存管理。本篇文章作为JVM内存管理系列的开篇,将聚焦于JVM内存区域的划分。通过这篇文章,读者将能够理解JVM内存的基本结构,为后续深入学习打下坚实的基础。
1 内存区域
JVM在执行Java程序的过程中,会把数据分到运行时数据区中不同的内存区域管理,包括程序计数器、虚拟机栈、堆、方法区、本地方法栈等等。
1.1 程序计数器
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
1.2 虚拟机栈
每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表:存放方法参数和局部变量,如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用
- 操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 动态链接:包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接
- 方法返回地址:返回至方法当前被调用的位置
1.3 本地方法栈
本地方法是由非java语言实现,比如C/C++。java语言本身无法调用很多的系统资源的,需要JVM和系统打交道,比如操作内存、处理文件、线程调度等,而这部分功能往往是使用C/C++写的,因此就需要用到传统的栈(C stack)来调用本地方法。
1.4 堆
堆区分为新生代和老年代,其中新生代又分为Eden区、Survior 1区、Survior 2区,新生代与老年代默认1:2,而Eden与Survivor区默认8:1:1
之所以新生代划分为多个区而老年代没有划分,也与各自的垃圾回收算法有关,新生代就选用复制算法,老年代使用“标记—清理”或者“标记—整理”算法来进行回收,这在后续介绍垃圾收集是会进行详细说明。
1.5 方法区
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,是存储元数据地方。
JDK8 之前,Hotspot 中方法区的实现是永久代,JDK8 开始使用元空间,以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。
为什么要使用元空间取代永久代的实现?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
2 运行时内存区域示例
以下面这段代码为例,分析运行时数据区的数据结构:
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
int c = (a + b) * 10
时,运行时数据区的结构如下:
3 JVM内存参数设置
在JVM的内存管理中,参数设置对于优化性能和减少垃圾回收(Garbage Collection, GC)至关重要。通过调整内存参数,可以影响JVM的内存使用方式,从而减少GC的发生频率和影响。因此这里将简要介绍JVM内存参数的基本概念和作用,为后续的性能优化专题打下基础。
关于新生代和老年代的JVM参数通常有一下设置:
-Xss:每个线程的栈大小,-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
-XX:+UseAdaptiveSizePolicy:自适应策略,会导致eden区和survior区的8:1:1比例自动变化(默认开启),如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
以eureka为例,Spring Boot程序的JVM参数设置格式如下:
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
4 总结
本文详细介绍了JVM内存区域的划分,包括程序计数器、堆、栈、方法区和本地方法栈。通过运行时内存区域的示例,我们了解了这些内存区域在实际程序中的表现。同时,我们提供了JVM内存参数设置的指南,帮助开发者根据应用程序的需求合理配置内存。在后续的文章中,我们将深入探讨内存分配、垃圾回收机制等话题,进一步揭示JVM内存管理的深层机制。