02 JVM 内存

Wu Jun 2020-02-24 16:27:26
05 Java > 01 Java 虚拟机

1 内存分区

JVM 所管理的内存分为以下几个区域

image

1.1 程序计数器

1.2 虚拟机栈

image

1.3 本地方法栈

同虚拟机栈相似,只不过是调用 Native 时用到。

1.4 堆

1.5 方法区

HotSpot 实现

1.6 运行时常量池

String.intern()

String.intern() 重用 String 对象

1.7 直接内存

2 堆中对象

HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。

2.1 对象的创建

虚拟机 new 指令

1)类加载

先检查常量池看是否为常量,否则若类未加载则执行类加载;

2)内存分配

为新生对象分配内存。

对象所需的内存大小在类加载完成后便完全确定,从 Java 堆中划分出对象空间。

3)初始化零值

虚拟机将分配到的内存空间都初始化为零值(不包括对象头)

4)设置对象头

设置对象的对象头信息

5)执行方法

把对象按照程序员的意愿进行初始化

2.2 对象的内存布局

对象在内存中存储的布局可以分为 3 块区域:对象头、实例数据和对齐填充;

1)对象头

对象头包括两部分信息:

如果对象是一个Java数组,对象头中还必须记录数组长度。

2)实例数据

实例数据存储代码中所定义的各种类型字段内容,包括父类中字段。

存储顺序受到虚拟机分配策略参数和字段在 Java 源码中定义顺序的影响。

3)对齐填充

对齐填充不是必然存在的,主要是由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。

2.3 对象的访问定位

栈上的 reference 类型目前主流的方式有句柄和直接指针两种。

1)句柄

Java 堆中划出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

最大好处是 reference 存储的是稳定的句柄地址,在对象被移动(垃圾收集时)时只改变句柄中实例数据指针,而 reference 本身不需要修改;

image

2)直接指针

reference 中存储的直接就是对象地址,在堆对象中放置访问类型数据的相关信息

最大好处在于速度更快,节省了一次指针定位的时机开销。

HotSpot采用该方式进行对象访问,但其他语言和框架采用句柄的也非常常见。

image

3 内存溢出异常 OOM

3.1 堆溢出

1) 异常报错

在 JVM 中如果 98% 的时间是用于 GC 且可用的 Heap size 不足 2% 的时候将抛出此异常信息

java.lang.OutOfMemoryError: Java heap space 
2) 堆参数

Heap size 大小 = 年轻代大小 + 年老代大小 + 持久代大小。

持久代一般固定大小为 64m,所以增大年轻代后,将会减小年老代大小。

提示:Heap Size 最大不要超过可用物理内存的 80%

3) 解决思路

先通过内存映像分析工具对 dump 出来的堆转储快照进行分析,先分清楚是内存泄漏还是内存溢出:

3.2 栈溢出

1) 异常报错

HotSpot 不区分虚拟机栈和本地方法栈

StackOverflowError
OutOfMemoryError

StackOverflowErrorOutOfMemoryError存在互相重叠的地方

2) 栈参数
3) 解决思路

虚拟机的默认参数对于通常的方法调用(1000~2000 层)完全够用,通常根据异常的堆栈日志就可以很容易定位问题。

如果建立过多线程导致内存溢出,在不能减少线程数的情况下,只能通过减少最大堆和减少栈容量来换取更多线程。

3.3 方法区溢出

1) 异常报错

若 Class 加载过多就可能报 PermGen space 。

java.lang.OutOfMemoryError: PermGen space 

常见于引用了大量的第三方 jar,对 JSP 进行 pre compile 时。

2) 栈参数

1.8 之前

1.8 之后

3.4 直接内存溢出

1) 异常报错

DirectMemory 导致的内存溢出,在 Heap Dump 里不会看见明显的异常。

2) 直接内存参数

-XX:MaxDirectMemorySize ,如不指定,默认与堆最大值一样

3) 解决思路

如果发现 OouOfMemory 之后 Dump 文件很小,程序又使用了 NIO,那就可以检查下是否这方面的原因。