1 内存分区
JVM 所管理的内存分为以下几个区域
1.1 程序计数器
- 线程私有,是当前线程所执行字节码的行号指示器。
- 执行 java 方法,记录正在执行的虚拟机字节码指令的地址;执行 native 方法,计数器为空。
- JVM 中唯一不存在 OutOfMemoryError 的区域。
1.2 虚拟机栈
- 线程私有,控制方法执行
- 方法执行时创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息
- 通常所讲的栈内存是指虚拟机栈中局部变量表部分,存放了基本数据类型、对象引用和 returnAddress 类型;
- 方法从调用到执行完成 == 栈帧在虚拟机栈从入栈到出栈。
1.3 本地方法栈
同虚拟机栈相似,只不过是调用 Native 时用到。
1.4 堆
- 存放对象实例,线程共享,空间最大。
- 内存回收角度:分代算法,新生代、老年代。
- 内存分配角度:可划分出多个线程私有的分配缓冲区。
- 不需要连续内存,可选择固定大小或可扩展
- 较新版本的 Java(从 Java 6 的某个更新开始)中,由于 JIT 编译器的发展和"逃逸分析"技术的逐渐成熟,栈上分配、标量替换等优化技术,使得对象不一定分配在堆上。
1.5 方法区
- 线程共享。存储已加载的类信息、常量、静态变量、即时编译后的代码等数据。
- 堆的一个逻辑部分,别名“非堆”
- 可选择不实现 GC,该区域对于垃圾收集来说条件比较苛刻,但是还是非常有必要要进行回收处理
HotSpot 实现
- 永久代(PermGen):jdk 1.8 之前,HotSpot 对方法区的永久代 GC 实现
- 元空间(Metaspace):jdk 1.8 之后元空间替代了永久代,使用本地内存
1.6 运行时常量池
- 方法区一部分
- 存储 Class 文件中编译器生成的各种字面量,符号引用,直接引用;
- 具备动态性,可以在运行期间将新的常量放入池中,如 String 的 intern 方法;
- JDK1.7 之后字符串常量池移到堆空间
String.intern()
String.intern() 重用 String 对象
- 已包含
返回常量池中字符串对象 - 未包含
- 1.6 之前,将字符串对象复制到方法区常量池,并返回方法区复制对象的引用
- 1.7 之后,将字符串对象的引用记录到堆中常量池,并返回此对象的引用。
1.7 直接内存
- 非 JVM 内存;
- NIO 可以使用 Native 函数库直接分配对外内存,通过 DirectByteBuffer 对象作为这块内存的引用进行操作以提升性能;
2 堆中对象
HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。
2.1 对象的创建
虚拟机 new 指令
1)类加载
先检查常量池看是否为常量,否则若类未加载则执行类加载;
2)内存分配
为新生对象分配内存。
对象所需的内存大小在类加载完成后便完全确定,从 Java 堆中划分出对象空间。
- 分配算法
- 带 Compact 过程的收集器(Serial、ParNew 等):指针碰撞
- 基于 Mark-Sweep 算法收集器(CMS):空闲列表
- 并发分配
- 一是同步分配动作(CAS + 失败重试);
- 二是为每个线程分配一小块内存(本地线程分配缓冲,TLAB),各个线程独立分配,只有 TLAB 用完需要分配新的才需要同步锁定,虚拟机通过 -XX:+/-UseTLAB 参数来设定;
3)初始化零值
虚拟机将分配到的内存空间都初始化为零值(不包括对象头)
4)设置对象头
设置对象的对象头信息
5)执行方法
把对象按照程序员的意愿进行初始化
2.2 对象的内存布局
对象在内存中存储的布局可以分为 3 块区域:对象头、实例数据和对齐填充;
1)对象头
对象头包括两部分信息:
- 自身运行时数据
如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等; - 类型指针
即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个Java数组,对象头中还必须记录数组长度。
2)实例数据
实例数据存储代码中所定义的各种类型字段内容,包括父类中字段。
存储顺序受到虚拟机分配策略参数和字段在 Java 源码中定义顺序的影响。
3)对齐填充
对齐填充不是必然存在的,主要是由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。
2.3 对象的访问定位
栈上的 reference 类型目前主流的方式有句柄和直接指针两种。
1)句柄
Java 堆中划出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
最大好处是 reference 存储的是稳定的句柄地址,在对象被移动(垃圾收集时)时只改变句柄中实例数据指针,而 reference 本身不需要修改;
2)直接指针
reference 中存储的直接就是对象地址,在堆对象中放置访问类型数据的相关信息
最大好处在于速度更快,节省了一次指针定位的时机开销。
HotSpot采用该方式进行对象访问,但其他语言和框架采用句柄的也非常常见。
3 内存溢出异常 OOM
3.1 堆溢出
1) 异常报错
在 JVM 中如果 98% 的时间是用于 GC 且可用的 Heap size 不足 2% 的时候将抛出此异常信息
java.lang.OutOfMemoryError: Java heap space
2) 堆参数
- -Xms(初始空间):推荐物理内存的 1/64
- -Xmx(最大空间):推荐物理内存的 1/4。
一般的要将 -Xms 和 -Xmx 选项设置为相同,以避免每次垃圾回收完成后 JVM 重新分配内存。 - -Xmn(年轻代大小):推荐配置为整个堆(-Xmx)的3/8。
Heap size 大小 = 年轻代大小 + 年老代大小 + 持久代大小。
持久代一般固定大小为 64m,所以增大年轻代后,将会减小年老代大小。
提示:Heap Size 最大不要超过可用物理内存的 80%
3) 解决思路
先通过内存映像分析工具对 dump 出来的堆转储快照进行分析,先分清楚是内存泄漏还是内存溢出:
- 内存泄漏:进一步查看泄漏对象到 GC Roots 的引用链,从而确认为什么无法回收;
- 内存溢出:则应当检查虚拟机堆参数或检查是否存在对象生命周期过长、持有状态时间过长的情况;
3.2 栈溢出
1) 异常报错
HotSpot 不区分虚拟机栈和本地方法栈
- 线程请求的栈深度大于虚拟机所允许的长度
StackOverflowError
- 虚拟机在扩展栈时无法申请到足够的内存空间
OutOfMemoryError
StackOverflowError
和OutOfMemoryError
存在互相重叠的地方
2) 栈参数
- -Xss(每个线程的栈容量)
3) 解决思路
虚拟机的默认参数对于通常的方法调用(1000~2000 层)完全够用,通常根据异常的堆栈日志就可以很容易定位问题。
如果建立过多线程导致内存溢出,在不能减少线程数的情况下,只能通过减少最大堆和减少栈容量来换取更多线程。
3.3 方法区溢出
1) 异常报错
若 Class 加载过多就可能报 PermGen space 。
java.lang.OutOfMemoryError: PermGen space
常见于引用了大量的第三方 jar,对 JSP 进行 pre compile 时。
2) 栈参数
1.8 之前
- -XX:PermSize 设置方法区大小
- -XX:MaxPermSize 限制方法区大小
1.8 之后
- -XX:MetaSpaceSize 初始空间大小,触发GC调整,多退少补
- -XX:MaxMetaspaceSize 最大空间,默认没有限制
3.4 直接内存溢出
1) 异常报错
DirectMemory 导致的内存溢出,在 Heap Dump 里不会看见明显的异常。
2) 直接内存参数
-XX:MaxDirectMemorySize ,如不指定,默认与堆最大值一样
3) 解决思路
如果发现 OouOfMemory 之后 Dump 文件很小,程序又使用了 NIO,那就可以检查下是否这方面的原因。