细说JVM内存模型
前言
在正式学习 JVM 内存模型之前,先注意以下几个是问题:
- JVM 内存模型与 JAVA 内存模型不是同一个概念。JVM 内存模型是从运行时数据区的结构的角度描述的概念;而 JAVA 内存模型是从主内存和线程私有内存角度的描述。从以下两张图可以看出:
JAVA内存模型
JVM内存模型
-
Java虚拟机总共由三大模块组成:
- 类加载器子系统
- 运行时数据区执行引擎
本篇我们介绍第二大模块——运行时数据区(JVM内存模型)。
-
其实虚拟机的这些模块并不是独立的,都是相互联系的。java 文件编译为 class 文件,通过类加载子系统加载,信息再到 JVM 托管的内存中(部分操作会与本地内存交互)的流转,再到垃圾回收等等,都是一系列的操作。
本系列的博客为了更加清晰的描述清楚功能和原理,将其分为几个章节写作。
概览
运行时数据区分为几大模块(如上图所示):
线程共享区:
- JAVA堆
- 方法区
线程私有区:
- JAVA栈
- 本地方法栈
- 程序计数器
本文中,我们将从以下几个方法面来分析各个区域:
- 功能
- 存储的内容
- 是否有内存溢出和内存泄露
- 是否进行垃圾回收
对应的垃圾回收算法- 垃圾回收流程
性能调优
线程私有区
程序计数器
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过该计数器的值来选择选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复都需要依赖该区域。
通俗点讲,该区域存放的就是一个指针,指向方法区的方法字节码,用来存储指向下一条指令的地址,也就是即将要执行的指令代码。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
当执行完一行指令码,JVM执行引擎会更新程序计数器的值。
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。(方法的调用,方法中又调用另外一个方法,正式满足栈的“先进先出,后进后出”的模型)。
OutOfMemoryError:无
虚拟机栈
它描述的是java方法执行的内存模型,其生命周期与线程相同。
每个方法在执行的同时都会创建一个栈帧(StackFrame),每一个栈帧又包括局部变量表、操作数栈、动态链接、方法出口等。方法的调用,方法中又调用另外一个方法,正式满足栈的“先进先出,后进后出”的模型。即每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
以上都只是几个很机械的概念,难以深入理解。下面我通过一个示例,来分析虚拟机栈的存储内容。
首先创建一个简单的程序:
package com.sunwin.robotcloud.test; /** * Created by 追梦1819 on 2019-11-01. */ public class CalculateMain { public int calculate(){ int a = 3; int b=4; int c = a+b; return c; } public static void main(String[] args) { CalculateMain main = new CalculateMain(); int d = main.calculate(); System.out.println(d); } }
对于以上程序,线程启动时,虚拟机会给主线程 main 分配一个大的内存空间,然后给main方法分配一个栈帧,存放该方法的局部变量;
执行calculate()方法时又分配一个calculate()的栈帧,存放对应方法的局部变量。
要注意的是,一个方法分配一个单独的内存区域,即栈帧。
Java 属于高级语言,难以直接通过代码看出它的执行过程。我们通过底层的字节码,反解析出执行的指令码,来分析底层执行过程。