(一)JAVA内存区域与内存溢出异常

JAVA内存区域与内存溢出异常

本节主要介绍,

  1. JAVA虚拟机的运行时内存模型和功能
  2. 各个模型内存溢出的情形和处理

运行时数据区域

JAVA程序要运行时,JVM中的内存区域如下:
JVM内存区域

这些区域都对应什么功能的空间?

程序计数器

程序计数器有什么作用?

当前线程所执行的字节码的行号指示器
循环、跳转、异常处理、线程恢复等基础功能都需要

程序计数器在多线程中起什么作用?

JAVA的多线程是通过线程轮流切换并分配处理器执行时间来实现的
任何时间,一个处理器内核只会执行一条程序指令
为了实现程序切换后能恢复到正常流程
每一个线程都需要一个独立的程序计数器
因此,程序计数器是线程私用

程序计数器会出现哪些错误?

当执行native方式是值为空
是JVM内存中唯一不会出现OutOfMemoryError错误的

JAVA虚拟机栈

JAVA虚拟机栈有什么作用?

描述JAVA方法执行的内存模型
方法在执行的同时会创建一个帧栈(内存)存储局部变量表、操作数、动态链接、方法出口等
方法从调用到执行完成就对应着入栈到出栈的过程
JAVA虚拟机栈是线程私用

局部变量表包含哪些?

各种基本类型数据、对象引用和returnAddress类型
long和double占用2个局部变量空间(slot),其余的占1个
进入方法后,局部变量表大小就确定了,之后都不会再变

虚拟机栈会出现哪些错误?

  • StackOverflowError:虚拟机栈长度固定,栈深度大于了虚拟机所允许的深度
  • OutOfMemoryError:虚拟机栈长度可以动态扩展,但是无法申请到足够的内存

本地方法栈

本地方法栈有什么作用?

和JAVA虚拟机栈功能类似,区别是:虚拟机栈执行JAVA方法服务,本地方法栈执行Native方法服务。
本地方法栈是线程私用

JAVA堆

JAVA堆有什么作用?

堆内存的唯一目的是存放对象实例和数组。同时Java堆也是GC管理的主要区域。

并不绝对

是垃圾回收的主要区域,也称为”GC堆”
回收的细节以后再说

JAVA堆会出现哪些错误?

虽然堆的内存是最大的,也是可实现可扩展的
但是如果没有空间无法再扩展时,就会抛出OutOfMemoryError错误

方法区

方法区有什么作用?

方法区是线程共享
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

方法区的内存管理在HotSpot中作为堆的一个“特例”来对待,免去额外的管理方法,但这样是有问题的——方法区的内存一般是不会被GC回收,受堆的内存限制影响,方法去也更容易遇到内存溢出的问题。
对方法区的实现不受虚拟机的规范

java7把字符串常量池移出到堆中

方法区会出现哪些错误?

当方法区无法满足类的存储时,抛出OutOfMemoryError错误

运行时常量池

属于方法区的一部分

class文件中除了类的版本、字段、方法、接口等描述信息外,还有一项信息是运行时常量池,用于存放编译时期生成的字面量和符号常量引用。

直接内存

什么是直接内存?

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。

实际上这部分是属于jvm堆的一部分

为什么需要直接内存?

java IO 过程:内核内存 → jvm 本地堆空间 → java堆
直接内存是jvm代码和java代码都可以访问的空间,可以减少一次从jvm到java的拷贝过程(后面一次)

直接内存的内存限制?

受用户空间堆大小的影响

HotSpot虚拟机对象探秘

本节的目的:
JVM内存区域是如何创建、如何布局以及如何访问的?

对象的创建

对象的创建过程?

  1. 定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  2. 为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定
  3. 将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。

什么是指针碰撞?什么是空闲列表?如何选择?

上面第二部分配内存的两种方式:

  1. 指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离
  2. 空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

选择哪种分配方式是由Java堆是否规整决定,而Java堆是否规整又由采用的垃圾收集器是否带有压缩整理功能决定。

如何解决堆空间分配时的并发问题?

问题:正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存
解决:一种是对分配内存空间的动作进行同步处理,另一种是把内存分配的动作按照线程划分在不同的空间之中进行

对象的内存布局

对象在内存中是如何布局的的?

对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐补充(Padding)。

对象头中有哪些信息?

  1. 自身的运行时数据,如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
  2. 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据中有哪些信息?存储的顺序如何?

对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内存。
无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。

对齐填充有什么意义?

不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。

对象的访问定位

什么是reference类型?

建立了对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。

由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。目前主流的方式有使用句柄和直接指针两种。

对象的访问方式:句柄访问和直接指针?有何区别?

  • 句柄访问:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
  • 直接指针:使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址

句柄访问

直接指针

区别:

  1. 使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
  2. 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

实战:OutOfMemoryError异常

本节的目的:

  1. 通过代码验证jvm中各个运行时区域存储的内容
  2. 能根据异常的信息快速判断是哪个区域的内存溢出,以及如何处理

堆溢出

堆溢出情形:
Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.king.oom;

import java.util.ArrayList;
import java.util.List;

/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {

static class OOMObject {}

public static void main(String [] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

如何解除堆溢出?

一般的手段是先通过内存映像分析工具对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。
如果是内存溢出,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的参数(-Xmx与-Xms)

虚拟机栈和本地方法栈溢出

由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。

栈溢出情形:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.king.oom;

/**
* VM Args: -Xss228k
*/
public class JavaVMStackSOF {

private int stackLength = 1;

public void stackLeak() {
System.out.println(stackLength ++);
stackLeak();
}

public static void main(String [] args) {
JavaVMStackSOF sof = new JavaVMStackSOF();
sof.stackLeak();
}
}

package com.king.oom;

/**
* VM Args:-Xss2M
*/
public class JavaVMStackOOM {

private void dontStop() {
while (true);
}

public void stackLeakByThread() {
while (true) {
new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
}).start();
}
}

public static void main(String [] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

解决方案:
如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的栈帧大小并不是一样的,所以只能说在大多数情况下)达到1000~2000完全没问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆,减少栈容量和减少最大方法区容量来换取更多的线程。

方法区和运行时常量池溢出

intern在java6和java7中的区别?

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

运行时常量池的溢出情形:

在JDK 1.6及之前的版本中,由于常量池分配在永久代内,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制常量池的容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.king.oom;

import java.util.List;

import com.google.common.collect.Lists;

/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
public static void main(String [] args) {
// 使用List保持着常量池引用 避免Full GC回收常量池行为
List<String> list = Lists.newArrayList();
// 10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}

方法区的溢出情形:
基本的思路是运行时产生大量的类去填满方法区,直到溢出。下面通过CGLIB实现方法区溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.king.oom;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM {
public static void main(String [] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invoke(obj, args);
}
});
enhancer.create();
}
}

static class OOMObject {}
}

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。

本机直接内存溢出

DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。

本文标题:(一)JAVA内存区域与内存溢出异常

文章作者:Sun

发布时间:2019年09月29日 - 15:09

最后更新:2019年10月16日 - 19:10

原始链接:https://sunyi720.github.io/2019/09/29/JVM/(一)JAVA内存区域与内存溢出异常/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。