写点什么

JVM 运行时数据区

用户头像
Alex🐒
关注
发布于: 2020 年 07 月 22 日





JVM 的内存在不同的运行时数据区进行操作。包括方法区(method area)、堆(heap)、栈(stacks)、程序计数器(program counter registers)、本地方法栈(native method stacks)。其中堆、方法区是线程共享的,栈、本地方法栈、计数器是线程隔离的。



程序计数器

线程私有的一块较小的内存空间,每个新的线程启动后,就会被 JVM 在分配自己的程序计数器,通过计数器来指示下一条指令执行。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域

虚拟机栈

当一个新线程启动的时候,JVM 会为线程创建独立的内存栈,其他的线程不能修改或访问该线程的栈帧。内存栈是由栈帧(Stack frame)构成,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息,栈帧的操作只有两种:出栈和入栈。



当一个线程调用某个 Java 方法时,JVM 创建并将一个新帧压入到内存栈中,这个帧成为当前栈帧,当该方法执行的时候,JVM 使用操作数栈来存储局部变量、中间计算结果等数据,只有当前线程可以访问, 所以不用担心多线程同步访问 Java 的局部变量。方法有两种方式而结束:正常结束或异常结束。不论哪种,JVM 都会弹出或者丢弃该栈帧,上一帧的方法就成为了当前帧。



JVM 允许指定 Java 栈的初始大小以及最大、最小容量。



实例代码

public class Demo {
public int math() {
int a = 1;
int b = 1;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.math();
}
}



查看反编译代码

javac Demo.java => javap -c Demo.class

截取 math() 方法内容,指令参考 Java 指令手册

public int math();
Code:
0: iconst_1
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn



执行过程图示



动态链接

在运行期间,由符号引用转化为直接引用。

查看反编译内容,有如下代码段,其中有#标示的内容

public static void main(java.lang.String[]);
Code:
0: new #2 // class com/alex/Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method math:()I
12: pop
13: return



通过 javap -v Demo.class 命令查看更多反编译内容

Constant pool 为常量池,#4 对应着常量池中的内容,即方法引用 com/alex/Demo.math:()I,即符号引用

Constant pool:
#1 = Methodref #5.#16 // java/lang/Object."<init>":()V
#2 = Class #17 // com/alex/Demo
#3 = Methodref #2.#16 // com/alex/Demo."<init>":()V
#4 = Methodref #2.#18 // com/alex/Demo.math:()I
...
#10 = Utf8 math
#11 = Utf8 ()I
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 SourceFile
#15 = Utf8 Demo.java
#16 = NameAndType #6:#7 // "<init>":()V
#17 = Utf8 com/alex/Demo
#18 = NameAndType #10:#11 // math:()I

当使用 new 新建一个对象时,对象存在于堆中,局部变量表存储对象的引用,指向堆中的对象,即直接引用

动态链接:在程序运行期间,由符号引用转化为直接引用

静态链接:在类加载解析阶段,由符号引用转化为直接引用

本地方法栈

若某线程正在执行一个本地 Java 方法(c/c++ 实现),该线程的本地方法内存栈中,保存了本地 Java 方法调用状态,其状态包括局部变量、被调用的参数、它的返回值、以及中间计算结果。 功能同虚拟机栈相同。

方法区

在 JVM 内部,所有的线程共享相同的方法区。静态变量、常量、类信息(所有字段、方法)、运行时常量池(整形-128~127,执行 String.intern() 方法的 String)都存储在方法区。



在 Java 8 以前方法区通常叫做持久代/永久代,在 Java 8 及以后成为元空间。



运行时常量池是动态的,会受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError: PermGen space 异常(Java 8 以后会抛出 OutOfMemoryError: Metaspace 异常)。

Java 堆在虚拟机启动的时候建立,是 Java 程序最主要的内存工作区域,也是垃圾收集器管理的主要区域。几乎所有的 Java 对象实例都存放在 Java 堆中,当对象无法在该空间申请到内存时,会抛出 OutOfMemoryError 异常。堆空间是所有线程共享的。

堆内存模型

堆内存空间作为 JVM 的数据集中管理区,存取效率、空间释放(GC)就成为了重中之重,JVM 通过多区分代的架构来完成这两个目标的达成,主要分为新生代(Eden+Survivor0/1)和老年代(Tenured Space),并且有一定的比例设定。



Java 8 之前共享内存结构

永久代(Permanent Generation)即方法区,与堆内存同为共享内存区。但是垃圾收集(GC)不会在主程序运行期对 PermGen space 进行清理,所以加载了很多 Class、过大的运行时常量池可能出现 PermGen space 错误。



Java 8 及之后共享内存结构

在 Java 8 中,永久代被移除,取而代之的是本地元空间(Metaspace),JVM 开始使用本地化的内存,来存放类的元数据。



为什么要分代

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。



分代垃圾回收采用分治的思想,进行代际的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

对象生命周期



每经过一次 YoungGC,age 增加1,通常 age > 15 时,对象会进入老年代,为什么是15次呢?



先看一下 Java 的对象头信息,Java 对象头在 32 位机和 64 位机的表现是不同的,看看 64 位机下的表现(现在普遍都是 64 位机)。



|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal Object |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased Object |
|------------------------------------------------------------------------------|--------------------|

age 由 4 bits 表示年龄,范围 0~15

内存溢出场景

Java 堆什么情况会溢出

堆内存大小由 -Xmx-Xms 来调节,如果程序使用的内存超过了堆最大内存(-Xmx),则堆内存溢出 java.lang.OutOfMemoryError: Java heap space。



Java 方法区什么情况会溢出

方法区(永久代)大小由 -XX:PermSize-XX:MaxPermSize 来调节,加载类过多、运行时常量池加入过多数据有可能使永久代溢出 java.lang.OutOfMemoryError: PermGen space。



Java 8 以后移除了方法区,取而代之的是本地元空间 Metaspace,大小由 -XX:MetaspaceSize-XX:MaxMetaspaceSize 来调节,溢出的错误也变为 java.lang.OutOfMemoryError: Metaspace。



Java 栈和本地方法栈什么情况会溢出

栈大小由 -Xss 来调节,方法调用层次太多会使栈溢出 java.lang.StackOverflowError。



另外,线程太多也会占满栈区域(线程内分配了自己的栈,但是进程中所有线程可使用的栈总大小是一定的)也会抛出异常:java.lang.OutOfMemoryError: unable to create new native thread






发布于: 2020 年 07 月 22 日阅读数: 61
用户头像

Alex🐒

关注

还未添加个人签名 2020.04.30 加入

还未添加个人简介

评论

发布
暂无评论
JVM 运行时数据区