笔记目录
1.JVM内存模型概览
虚拟机栈
:JVM运行过程中存储当前线程运行所需的指令、数据、引用地址等信息。
本地方法栈
:Java调用native方法时候的线程栈结构,部分虚拟机将本地方法栈和虚拟机栈合并。
程序计数器
:当前线程执行的字节码的行号指示器。
堆
:常规JVM最大的内存区域,逻辑上是连续的,堆保存了绝大部分的对象,少部分会是栈。堆也是GC发 生地之一。
方法区
:JVM的逻辑划分,不同版本有不同的实现。JDK8以前实现叫永久代
,现在叫元空间
的实 现。包括类的元数据信息、常量池等。
直接内存
:又称堆外内存
,他不是JVM虚拟机的一部分,但是Java程序可以操作这一部分区域。
2.虚拟机栈(简称:栈)
虚拟机栈
:它是一种FILO先入后出的数据结构模型,他的声明周期和当前线程一致,他的作用就是为 运行Java方法所需的指令、数据、引用的信息。JDK8栈默认大小是1024KB = 1MB
, JVM启动参数-Xss
命令可以指定栈的初始大小。但是Hotspot虚拟机不支持动态的栈扩 容,JVM官方定义栈是可以扩容的,Classic虚拟机就支持扩容栈。
2.1 栈的异常
2.1.1 StackOverflowError
如果栈的压栈深度超过栈的总大小,则发生StackOverflowError,常见的递归就容易造成。
2.1.2 OutOfMemoryError
如果JVM虚拟机支持动态扩容栈,那么如果栈在扩容时无法申请足够的空间,则会抛出该错误。如果不支持动态扩容栈,只有在JVM申请栈初始空间时候不足才会抛出。
2.2 栈的内存结构(栈帧)
栈是由多个栈帧
组成,通过栈的FILO原则进行压栈和出栈。
2.2.1 局部变量表
顾名思义,它就是一张表来存放数据,他的分配单元简称slot槽
,一个槽占用32位,例如int。如果是double和long类型则需要占用2个槽。局部变量表第0号位置是this
,因为方法都是对象调用的,当前对象就是this。
2.2.2 操作数栈
顾名思义,操作数栈也是一个栈结构,它的作用是存储方法在运行时执行引擎
需要计算的数据。现在的Hopspot虚拟机提供了TOS 栈顶缓存技术
,将操作数栈顶数据缓存在CPU寄存器中,提高执行效率。
2.2.3 动态链接
方法的符号引用指向运行时常量池,在类加载阶段,类的所有变量和方法都有符号引用,未执行的方法在静态链接阶段没有赋予直接引用(内存地址),动态链接的作用就是符号引用转化为直接引用的内存地址,还有非静态方法可能运行的时候调用的就是重写后的方法,执行体不一样,所以只有运行的时候才会去把符号引用转成地址。
2.2.4 返回地址
存储方法执行完,程序计数器该执行哪一个指令的地址。Java返回通常有2种形式:1.正常执行return
指令;2.遇到异常。
2.3 栈帧的运行流程
我新建一个类叫Demo1,然后通过javap反汇编命令,来看看:
/**
* @author Minor
*/
public class Demo1 {
public int test() {
int a = 10;
int b = 20;
int c = (a+b)*2;
return c;
}
}
wangzhi@wangzhideMacBook-Pro ~/Desktop/JavaBase/src/com/company/base javap -c Demo1
警告: 二进制文件Demo1包含com.company.base.Demo1
Compiled from "Demo1.java"
public class com.company.base.Demo1 {
public com.company.base.Demo1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int test(); // 我们java代码里定义的test()方法
Code: // 字节码指令
0: bipush 10 // 将常量10压入操作数栈
2: istore_1 // 将操作数栈的值10存储到局部变量表下标为1的位置
3: bipush 20 // 将常量20压入操作数栈
5: istore_2 // 将操作数栈的值10存储到局部变量表下标为2的位置
6: iload_1 // 将局部变量表下标为1的变量压入操作数栈
7: iload_2 // 将局部变量表下标为2的变量压入操作数栈
8: iadd // 加法运算,将操作数栈里的值进行求和
9: iconst_2 // 将值为2的常量压入操作数栈
10: imul // 乘法运算
11: istore_3 // 将操作数栈的值存储到局部变量表下标为3的位置
12: iload_3 // 将局部变量表下标为3的变量压入操作数栈
13: ireturn // 方法返回
}
3.程序计数器
程序计数器是用来记录线程执行字节码的行号地址
,因为现代计算机的工作模式基于CPU的时间片轮转机制,线程在执行程序的时候难免会遇到CPU调度问题,此时就需要一个地方来存储线程当前执行的位置。显然,每个线程各自独立,都有属于自己一份的程序计数器。由于结构简单,功能单一,程序计数器也是JVM内存模型中唯一不会发生内存溢出
的地方。需要值得注意的点是当java线程在执行本地方法(native修饰的方法)时,程序计数器并不会记录执行位置,因为操作系统层面也有一个程序计数器,本地方法依靠它去记录。
4.本地方法栈
Java调用native方法时候的线程栈结构,部分虚拟机将本地方法栈和虚拟机栈合并。
5.方法区
方法区
:《Java虚拟机规范》 规定属于堆
的逻辑部分,但是他的存储模型却是和堆分开的,它是线程共享区域,存储类的元数据信息、静态变量、JIT的编译缓存等。Hotspot在JDK8之前方法区这个逻辑区域的实现称之为永久代
,后来的实现叫做元空间
,并且元空间使用本地内存。在JDK7以后,方法区中的字符串常量池、静态变量等信息已经挪到了堆中,但是我们一般认为类信息、静态的数据仍然属于方法区。
-XX:MetaspaceSize
:设置元空间初始大小。
-XX:MaxMetaspaceSize
:设置元空间最大大小。
元空间当无法满足申请的内存时,会抛出OutOfMemoryError
。
5.1 运行时常量池
每一个类和接口在编译期生成的各种字面量和符号引用在类加载之后就会存放到方法区中的运行时常量池
中。在程序运行时,运行期间也可将新的常量放入池中,例如String的intern()方法。
6.堆
堆 Heap
:常规情况下是JVM内存最大的区域,绝大部分的Java对象都存放在堆中,极少部分可能会在栈中(栈上分配)。堆空间也是GC的主要关注点,HotSpot堆空间的大小也是支持动态扩容。
《Java虚拟机规范》中规定,堆可以是物理不连续的内存空间,但是在逻辑上是连续的。当堆空间申请不够的内存时将抛出OutOfMemoryError
错误。
-Xms
:设置堆空间最小值。
-Xmx
:设置堆空间最大值。
-XX:NewSize
:设置新生代的最小值。
-XX:MaxNewSize
:设置新生代的最大值。
6.1 垃圾收集器、GC、对象内存布局(后续文章安排)
7.直接内存/堆外内存
堆外内存
:并不是JVM规范中的内存区域,但是Java许多类库使用到了堆外内存。例如NIO中DirectByteBuffer对象可以操作堆外内存,堆外内存受到物理内存限制,可以通过-XX:MaxDirectMemorySize
设置。常用的NIO、JNI通过unsafe操作直接内存,第三方框架如Ehcache也使用了直接内存。直接内存也是内存泄漏严重的区域,因为直接内存不受到GC管制。