JVM(Java Virtual Machine,Java虚拟机)对于Java开发者和运行 Java 应用程序而言至关重要。其重要性主要体现在跨平台性、内存管理和垃圾回收、性能优化、安全性和稳定性、故障排查与性能调优等方面。今天就下学习一下 JVM 的内存模型。
一、JVM内存模型
JVM 内存模型(Java Virtual Machine Memory Model,简称JMM)是 Java 虚拟机规范定义的一种内存结构组织方式,用于规定 Java 程序在 JVM 中的运行时数据区域划分以及它们之间的关系。它不仅描述了如何划分内存区域,还规定了线程如何访问这些内存区域以及线程间的通信规则。JMM 如下图:
JVM内存模型主要分为程序计数器、Java虚拟机栈、本地方法栈、堆、方法区、运行时常量池、直接内存,下面来分别看下。
二、程序计数器
程序计数器(Program counter Register)是一块较小的内存空间,它可以看做是当前线程所执行字节码的指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,它是程序流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要这个计数器来完成。每条线程独立拥有程序计数器,各线程之间计数器互不影响。
如果线程正在执行 java 方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是本地(native)方法,这个计数器值则应为空。程序计数器是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemeryError 情况的区域。
三、 java虚拟机
与程序计数器一样,java 虚拟机栈也是线程私有的,他的生命周期与线程一样。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法的出口等信息。每一个方法在虚拟机栈中的执行完毕过程,就对应这一个栈帧从入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(byte、short、char、int 、float、double、long、boolean)、对象引用(refrence类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象句柄或者其他与此对象相关的位置)和returnAddress类型。
下面看一个例子
public class Test {
public static int cacl() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
int cacl = cacl();
System.out.println(cacl);
}
}
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
操作数栈是一个先进后出的栈,当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种各样的字节码指令往操作数栈中写入和提取,也就是出栈和入栈。
动态链接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。Class文件中的常量池存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。
四、 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用非常相似,其区别只是虚拟机栈为虚拟机执行 java 方法服务,而本地方法栈则是为虚拟机使用本地方法服务。
五、 堆
Java 堆(Java Heap)是虚拟机所管理的内存最大的一块。java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的是存放对象实例。
new 出来的对象是放到了 Eden 区,老年代默认占整个堆的 2/3 空间。JVM的分代理论中什么条件下会从新生代晋升到老年代?
- 默认情况下,对象经历了15次 minor gc,年龄变为15就会变为老年代。
- 假如说当前放对象的Survivor 区域里一批对象的总大小大于了这块 Survivor 区域的内存大小的50% ,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。
- 一些大对象在创建是也会放到老年代。老年代的包括一些链接,比如连接池等。
六、 方法区
方法区(Method Area)与 java 堆一样是线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态常量、即时编译器编译后的代码缓存等数据。方法区是JVM的一个逻辑部分,具体实现在java7之前是永久代,Java8之后是元空间。
那元空间和永久代有什么区别呢?
JDK8 及以后把类的元数据放在本地内存中,这一块区域叫做 Metaspace,该区域在 jdk7 及以前是属于永久代的,元空间和永久代都是用来存储 class 相关信息,包括 class 对象的method、field 等。元空间和永久代其实都是方法区的实现,只是实现不同,所以说方法区只是JVM的规范。
当前的主流框架,如 spring 等对类进行增强时都会使用 CgLib 等字节码技术,当增强的类越多,就需要越大的方法区,以保证动态生成的新类能载入内存。经常在运行时生成大量动态类的应用场景,应特别注意这些类的回收情况。
在 java7 之后,原先位于方法区里的字符串常量池也被移到了堆中。并在 Java8 中使用元空间代替了永久代。这一替代最大的区别是元空间使用的是本地内存,而永久代使用的是 jvm 内存,使用本地内存的好处是J ava.lang.outofMemoryError:PermGen Space 将不复存在,只受限于本地内存大小的限制,解决了空间不足的问题,不过也不可能任其无限大,jvm在默认运行时会根据需要动态的设置其大小,这一替换的好处如下:
- 字符串常量池在永久代中容易出现性能问题和内存溢出问题。
- 类的方法信息大小难以确定,因此给永久代的大小指定带来了困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。永久代会为 GC 带来不必要的复杂性,并且回收效率偏低,在永久代中元数据可能会随着每一次 full gc 发生而进行移动,而 hotspot 虚拟机每种类型的垃圾回收器都要特殊处理永久代中的元数据,分离出来后可以简化 full gc。
如下为永久代合堆的存储位置
七、 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项常量池,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。
运行期间也可以将新的常量放入池中,比如String的intern()方法
八、 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常的出现。
在 JDK1.4 中新加入的 NIO 类,引入了一种基于通道(channel)与缓存区(Buffer)的 I/O方式,它可以使用 Native 函数库直接分配内存,然后通过一个存储在 Java 堆里面的DirecByBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提升性能,因为避免了在 java 堆和 native 堆中来回复制数据。
显然,本地直接内存分配不受 java 堆大小的限制,但是既然是内存,则肯定还是会受到本机总内存大小及处理器寻址空间的限制。
因为通常的垃圾收集日志等记录,并不包含 Direct Buffer 等信息,所以 Direct Buffer 内存诊断也是个比较头疼的事情,在JDK 8 之后的版本,可以方便的使用 Native Memory Tracking(NMT) 特性来诊断,可以在程序启动时增加下面的参数:
-XX:NativeMemoryTracking={summary|detail}
注意,激活NMT通常会导致JVM 5%~10%的性能下降,需要慎重使用。
总结:了解了JVM的内存模型后才能更好的理解对象创建、垃圾回收等等功能,后续将继续介绍这部分内容。
往期经典推荐
实时数据传输的新里程——Server-Sent Events(SSE)消息推送技术-CSDN博客
SpringBoot项目并发处理大揭秘,你知道它到底能应对多少请求洪峰?_springboot并发处理-CSDN博客
领航分布式消息系统:一起探索Apache Kafka的核心术语及其应用场景-CSDN博客