本篇笔记不定期更新
内存结构图
先上图
这张图差不多就把JVM的内存结构以及和class源文件,JVM执行引擎,以及操作系统自带的本地方法接口之间的关系囊括进去了。
下面我们就JVM内存结构的几个组成部分来逐一简单介绍一下
1. 程序计数器
1.1 定义
Program Counter Register 程序计数器(寄存器)
1.2 作用
- 在执行当前指令时,记住下一条JVM指令的执行地址,所以PCR也被称为寄存器,在物理上通过CPU寄存器实现
- 在多线程程序中起到一个记录上下文的作用,方便切换线程时可以继续运行
1.3 特点
- Java支持多线程,而PCR是线程私有的,每个线程都有自己的PCR
- 不会存在内存溢出
2. 虚拟机栈
栈–线程运行需要的内存空间,由栈帧组成,栈帧看为栈内的元素
栈帧–每个方法运行时所需要的内存,参数,局部变量,返回地址等等····
当调用某个方法时,会给栈帧分配内存,并将这个栈帧压入栈中,运行完毕后,会释放内存,也就是弹出栈帧。
当方法调用方法时,就会压入多个栈帧
2.1 定义
Java Virtual Machine Stacks
- 每个线程运行时需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)祖传,对应着每次方法调用时所占用的内存,一般来说是局部变量
- 每个线程只能由一个活动栈帧,对应着当前正在执行的那个方法,也就是目前栈顶的那个栈帧
问题辨析
-
垃圾回收是否涉及栈内存?不涉及,栈内存只和方法有关,方法运行完毕后栈帧出栈,内存自动回收。
-
栈内存分配越大越好吗?不是,内存分配得越大,线程就越少,因为物理内存是确定的,内存分配的越大,能更多次的进行方法递归。一般来说,默认的分配内存已经够用了。
-
方法内的局部变量是否线程安全?
是的,因为每个线程只对应一个虚拟机栈,和其他方法的线程是互不干扰的,本质上操作的局部变量完全没有关系,因此是线程安全的。但如果不是局部变量,而是静态变量,或者是方法参数、或者是局部变量作为返回值返回了,那么就线程不安全,因为这时不同线程操纵的变量是同一个变量了。
简单来说,如果方法内局部变量没有逃离方法的作用范围,那么他就是线程安全的
2.2 栈内存溢出
-Xss 栈内存分配大小命令
StackOverflowError
- 栈帧过多导致栈内存溢出,如递归调用但没设置中止条件,或是出现了循环引用问题
- 栈帧过大导致栈内存溢出
2.3 实际演示
在idea中,通过断点调试,我们可以观测到栈帧的存在
运行如下代码
public class StackTest {
public static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
add(1, 2); //在这句打断点
}
}
程序运行到断点处停止,我们看debugger窗口
可以看得到Frames这个子窗口,这表示的就是栈帧的集合,也就是虚拟机栈,我们的程序在main方法中的add暂停,所以目前栈帧中只有一个main方法,我们运行到下一步看看
可以看到main栈帧的上方有了add,这符合栈后进先出的特点,可以预见,如果addd中继续调用方法,那么add栈帧之上又会有新的栈帧。
现在我们继续运行程序,让add方法运行完毕
可以看到add的栈帧消失了,也就是add方法的运行内存被释放了。
这个小demo可以看出栈帧扮演的角色和运行方式。
3. 本地方法栈
Native Method Stacks
本地方法:Native Method ,不是Java编写的代码,通常是操作系统自带的方法代码。
本地方法栈为本地方法的运行提供内存空间
4. 堆
4.1定义
Heap 堆
- 通过new关键字,创建对象都会使用堆内存
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2 堆内存溢出
-Xmx 堆内存分配大小命令
OutOfMemoryError : Java heap space
垃圾回收:没人用的对象,就作为垃圾被回收
堆内存溢出:大量的对象被不断创建,同时一直被使用,可能导致堆内存溢出
4.3 堆内存诊断
在idea控制台窗口即可运行
- jps工具
- 查看当前系统中有哪些Java进程
- jmap工具
- 查看堆内存占用情况 ,不连续,只能查看某一时刻的情况
- jmap -heap 进程id
- jconsole
- 图形界面,多功能检测工具,可连续监测
案例:
- 垃圾回收后,内存占用率仍然很高
- 使用jvisualvm工具,通过堆dump功能查看对象在某一时刻的具体情况,从而做出诊断
5. 方法区
5.1 定义
Method Area
方法区是所有Java虚拟机线程共享的区域,它存储了与类结构相关的信息,如成员变量,方法数据,成员方法和构造器的代码部分,运行时常量池。
方法区在虚拟机启动时就被创建,逻辑上它是堆的组成部分,但具体实现不同的jvm有所不同
5.2 组成
参照官方笔记
5.3 方法区内存溢出
-XX:MaxMetaspaceSize 设置元空间大小
OutOfMemoryError:Metaspace
- 1.8以前会导致永久代内存溢出
- 1.8以后会导致元空间内存溢出
5.4 运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5 StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder(jdk1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8中,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6中,将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则会把此对象复制一份,再放入串池,会把串池中的对象返回
5.6 StringTable位置
- 1.8,StringTable在堆(Heap)中
- 1.6,StringTable在永久代(PermGen)中
5.7 StringTable垃圾回收
5.8 StringTable性能调优
- StringTable本质上是哈希表,因此调优就是调整桶的个数,适当的把桶的个数调大,减少哈希碰撞 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
6. 直接内存
6.1 定义
Direct Memory
- 常见于NIO操作,用于数据缓存区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
6.3 分配和回收原理
- 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用的freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会有ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存