Java内存区域与内存溢出

前言

最近在读周志明老师的《深入理解Java虚拟机》,感觉一下换了一个角度来看待Java代码,有必要整理一些内容,更清楚实际的流程,这一篇就记录下Java内存区域与相关的一些内存溢出的异常。

内存区域

Java虚拟机在执行Java程序的过程会把它管理的内存划分为各个不同的区域,这些区域都有着各自的生命周期,总的来说Java虚拟机管理的内存将会包括一下的数据区域
运行时数据区域

图中可以很清晰的看出区域里面各个实体的关系,然后简单介绍一3下各个实体。

1.程序计数器

线程私有,可以看作是当前线程所执行的字节码的行号指示器,字节码解释器的工作就是通过改变计数器的值来进行后续的操作。

2.虚拟机栈

线程私有,描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。

3.本地方法栈

与虚拟机栈发挥的作用类似,但虚拟机栈主要是为执行Java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。

4.Java堆

是Java虚拟机所管理的内存中最大的一块,也是被所有线程共享的一块内存区域,在虚拟机启动时创建,此区域唯一目的是存放对象实例。

5.方法区

也是各个线程共享,用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,为堆的一个逻辑部分。其中的运行时常量池相对于Class文件常量池而言具备动态性,运行期间也可将新的常量放入池中。
除了这些以外,直接内存的不合理分配也会影响到Java虚拟机动态扩展内存时出现内存溢出。

从JVM看对象

Java对象的创建我们无时无刻都在使用着,这里从虚拟机层面来看待对象的创建。


对象的创建

1.当虚拟机收到一条new指令时,首先去检查这个指令的参数能否在常量池中定位到对应的符号引用,并且检查符号引用代表的类是否加载过、解析和初始化过,没有则进行对应的类加载过程。

2.在类检查通过后,虚拟机为新生对象从Java堆中分配内存。而内存的分配又有两种方式。一种是指针碰撞,就是把空闲和正在使用的内存中间放一个指针隔开,所以这种方式实际就是把指针进行移动,当内存出现相互交错时。该方式自然就行不通了;另一种方式是空闲列表,由虚拟机维护一个列表,分配内存后就更新这个列表的记录。至于使用哪种方式就要看是基于何种垃圾回收器而言。除了怎么划分可用空间之外,还需要考虑虚拟机分配内存的频率,分配频率就会涉及到并发中的一些问题了。JVM采用CAS(比较并交换)方式进行失败重试保证操作的原子性;另一种是每个线程在Java堆中预先分配一小块内存,也就是本地线程分配缓冲(TLAB)

3.内存分配完成后,虚拟机将分配到的内存空间都初始化为零值,使用TLAB,则可以提前到分配时进行。

4.虚拟机对对象进行必要的设置,也就是把该对象相关的信息存储在对象头之中,这些工作都完成后,一个新的对象已经产生了,接下来就是对象的初始化了。

对象的访问定位

对象的访问目前主流的方式有句柄和直接指针两种。
1.使用句柄访问,Java堆会划分出一块内存作为句柄池,对象的引用中存储的就是其句柄的地址,句柄包含了对象实例数据和类型数据的具体地址信息。
2.使用直接指针,堆就要考虑如何放置访问类型数据相关的信息,引用中存储的直接就是对象地址。
很显然,因为是直接指向地址,所以直接指针的方式更加快,但对象访问在Java中太过频繁,积少成多后也会造成较高的执行成本。

内存溢出异常

1.Java堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

这里可以设置下虚拟机相关的参数,

1
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF stackSOF = new JavaVMStackSOF();
try {
stackSOF.stackLeak();
} catch (Throwable e) {
System.out.println("Stack length: "+stackSOF.stackLength );
throw e;
}
}
}

3.创建线程导致的内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JavaVMStackOOM {
private void doStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(()->{
doStop();
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

这里的话,在Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上,这里可能导致机器假死。

4.方法和运行时常量池溢出

1
2
3
4
5
6
7
8
9
10
11
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}

5.本机直接内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DirectMemoryOOM {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
try {
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

总结

了解了这些不得不佩服设计者,一个看似简单的东西底层要解决的问题、处理的细节也是非常多的,从Java虚拟机层面看到了不一样的东西,也是非常有意思的,理解底层实现的原理有助于写成更健壮的代码,更快速的debug,就到这里了。

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 前言
  2. 2. 内存区域
    1. 2.1. 1.程序计数器
    2. 2.2. 2.虚拟机栈
    3. 2.3. 3.本地方法栈
    4. 2.4. 4.Java堆
    5. 2.5. 5.方法区
  3. 3. 从JVM看对象
    1. 3.1. 对象的创建
    2. 3.2. 对象的访问定位
  4. 4. 内存溢出异常
    1. 4.1. 1.Java堆溢出
    2. 4.2. 2.虚拟机和本地方法栈溢出
    3. 4.3. 3.创建线程导致的内存溢出
    4. 4.4. 4.方法和运行时常量池溢出
    5. 4.5. 5.本机直接内存溢出
  5. 5. 总结
,