【重要】第二章:Java内存区域与内存溢出异常

2018-12-16  本文已影响0人  linyk3

Java VS C++ : 内存动态分配和垃圾收集技术

2.1 概述

C/C++: 自己分配,自己维护
Java: 虚拟机分配,虚拟机维护. 出现内存溢出或内存泄露需要了解虚拟机机制才能够排查问题.

memory leak会最终会导致out of memory!

2.2 运行时数据区域

Java 虚拟机在执行程序的过程中会把它管理的内存划分为若干个不同的数据区域.各自都有自己的用途,创建时间销毁时间.

Java 虚拟机运行时数据区

2.2.1 程序计数器

程序计数器(Program Counter Register), 是一块较小的内存空间,可以看作是当前程序所执行的字节码的行号指示器.

虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.
Java 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器的内核都只会执行一条线程中的指令. 每条线程都需要有一个独立的程序计数器.这类内存区域被称为"线程私有"的内存.

2.2.2 Java 虚拟机栈

跟程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的.它的生命周期和线程相同.

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储:

每一个方法从调用直到执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程.
方法调用=>执行完成 ===== 虚拟机栈: 入栈 => 出栈

Java虚拟机规范中对这个区域规定了2种异常情况:

2.2.3 本地方法栈

本地方法栈(Native Method Stack) 与虚拟机栈的作用格式相似的,区别在于:

2.2.4 Java 堆

Java 堆(Java Heap) 是Java 虚拟机所管理的内存中最大的一块.
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.
Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存.
Java虚拟机规范中规定:所有的对象实例以及数组都要在堆上分配.但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术等使得对象都要在堆上分配渐渐变得不那么绝对.
Java 堆是垃圾收集器管理的主要区域,因此Java堆也被成为GC堆.
Java 堆区域细分:

Java 虚拟机规范中规定:Java堆可处于物理上不连续的内存空间中,只要逻辑上是连续的就可以.实现时可以是固定大小的,也可以是可扩展的. 目前主流的虚拟机是按照可扩展的(可以通过 -Xmx(最大堆内存) 和 -Xms 控制(最小堆内存))
如果堆中没有内存来完成实例分配, 且无法扩展时,将会抛出 OutOfMemoryError异常.

2.5 方法区

方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域,用来存储已被虚拟机加载的:

虽然Java虚拟机规范把方法区描述为Java 堆的一个逻辑部分.但有一个别名就做Non-Heap, 从而与Java 堆区分开来.

HotSpot 虚拟机用垃圾收集器中的永久代的方法来管理方法区的内存,这样就可以像管理Java堆中的内存一样来管理方法区的内存了. 所有HotSpot虚拟机上的方法区可以简单的看成是永久代. 其他的虚拟机(BEA JRockit等)不存在永久代的概念.
使用永久代来实现方法区,会更容易出现内存溢出问题,因为永久代有 -XX:MaxPermSize 上限.
JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出.

Java 虚拟机堆方法区的限制更宽松:内存不需要物理连续,可以选择固定大小或可扩展, 还可以选择不实现垃圾收集.当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常.

2.2.6 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分.
Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用.这部分内容将在类加载后进入方法区的运行时常量池中存放.

Class 文件:

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。

Java 虚拟机严格规定Class 文件中的每一部分(包括Class文件中的常量池表).每一个字节用于存储哪种数据都必须符合要求才能被虚拟机认可,装载和执行.

但是对于运行时常量池,Java虚拟机规范没有做任何细节的要求.

Class 中的常量池 -- 类加载 --> 方法区中的运行时常量池
运行期间生成的新的常量 -- 程序运行 --> 方法区中的运行时常量池

当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常.

2.2.7 直接内存

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域. 但是这部分内存也被频繁使用,也可能导致 OutOfMemoryError异常出现,所以放在这里一起讲.

JDK1.4 新加入的NIO类(New Input/Output), 引入了一种基于通道(Channel) 与缓冲区(Buffer) 的I/O 方式,可以使用Native函数库直接分配堆外内存,然后通过存储在Java 堆中的DirectByteBuffer 对象作为这个内存的引用进行操作.这样可以避免在Java堆和Native堆中来回复制数据.

本机直接内存不会受到Java堆内存的限制,但是会受到本机实际总内存以及处理器寻址空间的限制.

服务器管理员在配置虚拟机参数时, 会根据实际内存设置 -Xmx等参数,但是经常会忽略直接内存,从而导致动态扩展时出现 OutOfMemoryError 异常.

2.3 HotSpot 虚拟机对象探秘

探讨HotSpot虚拟机在Java堆中对象分配,布局和访问的全过程:

2.3.1 对象的创建

new -> 常量池是否存在类的符号引用 -> 类加载 -> 分配内存(指针碰撞/空闲列表)-> 解析 -> 初始化为0 -> 设置对象头 -> init方法 -> 使用 -> 销毁

分配内存时解决并发问题:

2.3.2 对象的内存布局

HotSpot虚拟机中的对象内存:


对象的内存布局 image.png

2.3.3 对象的访问定位

Java 程序通过栈上的reference 数据来操作堆上的具体对象.目前主流的访问方式有使用句柄和指针两种方式:

通过句柄访问对象

句柄优势:reference存储的是稳定的句柄地址,对象被移动时(经常是GC时移动)只需改变句柄中的实例数据指针,不需要修改reference.

直接指针优势:访问速度更快,节省了一次指针定位的时间开销.
HotSpot 使用的是直接指针访问.

2.4 实战: OutOfMemoryError 异常

除了程序计数器外,虚拟机的其他几个运行时区域都可能发生 OutOfMemoryError异常.

本节内容目的:

2.4.1 Java 堆溢出

Java 堆是存储对象的,只要不断的创建对象,并且保证GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,到达最大堆的容量后就会产生内存溢出.
实战是可以通过设置堆的大小为20M: -Xms20m -Xmx20m (-Xms == -Xmx => 不可扩展)

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

public class JVCode_2_3_HeapOOM {
    
    static class OOMObject {
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}
image.png
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid16168.hprof ...
Heap dump file created [27713936 bytes in 0.168 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:265)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
    at java.util.ArrayList.add(ArrayList.java:462)
    at JVCode_2_3_HeapOOM.main(JVCode_2_3_HeapOOM.java:14)

关于使用Eclipse MAT分析内存溢出的细节请查看:Eclipse 使用MAT(Memory Analyze Tool)

上一篇下一篇

猜你喜欢

热点阅读