JVM 运行时数据区
JVM 的内存在不同的运行时数据区进行操作。包括方法区(method area)、堆(heap)、栈(stacks)、程序计数器(program counter registers)、本地方法栈(native method stacks)。其中堆、方法区是线程共享的,栈、本地方法栈、计数器是线程隔离的。
程序计数器
线程私有的一块较小的内存空间,每个新的线程启动后,就会被 JVM 在分配自己的程序计数器,通过计数器来指示下一条指令执行。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈
当一个新线程启动的时候,JVM 会为线程创建独立的内存栈,其他的线程不能修改或访问该线程的栈帧。内存栈是由栈帧(Stack frame)构成,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息,栈帧的操作只有两种:出栈和入栈。
当一个线程调用某个 Java 方法时,JVM 创建并将一个新帧压入到内存栈中,这个帧成为当前栈帧,当该方法执行的时候,JVM 使用操作数栈来存储局部变量、中间计算结果等数据,只有当前线程可以访问, 所以不用担心多线程同步访问 Java 的局部变量。方法有两种方式而结束:正常结束或异常结束。不论哪种,JVM 都会弹出或者丢弃该栈帧,上一帧的方法就成为了当前帧。
JVM 允许指定 Java 栈的初始大小以及最大、最小容量。
实例代码
查看反编译代码
javac Demo.java
=> javap -c Demo.class
截取 math()
方法内容,指令参考 Java 指令手册
执行过程图示
动态链接
在运行期间,由符号引用转化为直接引用。
查看反编译内容,有如下代码段,其中有#
标示的内容
通过 javap -v Demo.class
命令查看更多反编译内容
Constant pool 为常量池,#4
对应着常量池中的内容,即方法引用 com/alex/Demo.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 位机)。
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
版权声明: 本文为 InfoQ 作者【Alex🐒】的原创文章。
原文链接:【http://xie.infoq.cn/article/e19e8e54ee9b153c84f5cb4f5】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论