JVM运行时内存区域
JVM 内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险,使 Java 开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。
JVM 自动内存分配管理机制的好处很多,但实则是把双刃剑。这个机制在提升 Java 开发效率的同时,也容易使 Java 开发人员过度依赖于自动化,弱化对内存的管理能力,这样系统就很容易发生 JVM 的堆内存异常,垃圾回收(GC)的方式不合适以及 GC 次数过于频繁等问题,这些都将直接影响到应用服务的性能。
JVM的内存划分中,有部分区域是线程私有的,有部分是属于整个JVM进程;有些区域会抛出OOM异常,有些则不会,了解JVM的内存区域划分以及特征,是定位线上内存问题的基础。那么JVM内存区域是怎么划分的呢?
Java虚拟机运行时数据区 (仅参考模型)
Java虚拟机运行时数据区-大概比例
1. Java内存区域
Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间,有的随着虚拟机进程启动而存在,有的区域则依赖于用户线程的启动和结束而建立和销毁。
根据《Java 虚拟机规范》的规定,Java 虚拟机所管理的内存一共分为 Program Counter Register(程序计数器)、VM Stack(虚拟机栈)、Native Method Stack(本地方法栈)、Heap(堆)、Method Area(方法区)五个区域。
1.1 程序计数器(Program Counter Register)
程序计数器
(Program Counter Register),简称PC,在JVM规范中,每个线程都有自己的程序计数器。
这是一块比较小的内存空间,存储当前线程正在执行的Java方法的JVM指令(或操作码)地址,即字节码的行号。如果当前方法是 native 方法,那么程序计数器的值为 undefined。
举个例子,下面是一段输出 Hello World 的代码,代码和反编译后的文件如下:
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello, World!");
}
}
Classfile /home/wkq/workspaces/java_test/HelloWorld.class
Last modified Jun 21, 2020; size 427 bytes
SHA-256 checksum cc0de3eb12561fe81dfb95961d817665276587839bdf652b6f4070c1b9942f10
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // HelloWorld
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello, World!
#14 = Utf8 Hello, World!
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // HelloWorld
#22 = Utf8 HelloWorld
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 HelloWorld.java
{
public HelloWorld();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello, World!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "HelloWorld.java"
LineNumberTable,是指每一个java字节码指令对应java代码文件中的第几行,以方便定位。
LineNumberTable:
line 3: 0
line 4: 8
其中 line 3 line 4 指的的代码中的行号, 0 8 指的是 java字节码的指令
该内存区域是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的内存区域。
1.2 Java虚拟机栈(Java Virtal Machine Stack)
Java虚拟机栈(Java Virtal Machine Stack),同样也是属于线程私有区域,该区域存储着局部变量表,编译时期可知的各种基本类型数据、对象引用、方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余的数据类型占1个。
局部变量表所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。
每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。
栈可以是动态分配也可以固定大小。
如果线程请求一个超过允许范围的空间,就会抛出一个StackOverflowError。
如果线程需要一个新的栈帧,但是没有足够的内存可以分配,就会抛出一个 OutOfMemoryError。