美文网首页
【重要】第二章:Java内存区域与内存溢出异常

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

作者: linyk3 | 来源:发表于2018-12-16 16:56 被阅读0次

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

    2.1 概述

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

    • 内存溢出:Out Of Memory,OOM。是指程序在申请内存时,没有足够的内存空间供其使用。
    • 内存泄漏:Memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

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

    2.2 运行时数据区域

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

    Java 虚拟机运行时数据区
    • 程序计数器:线程私有,存储当前程序所执行的字节码的行号指示器。
    • Java虚拟机栈:线程私有,为虚拟机执行Java方法服务,存储局部变量表,操作数栈,动态链接,方法出口等。
    • 本地方法栈:线程私有,为虚拟机执行Native方法服务。与Java虚拟机栈类似。
    • Java堆:线程共享,存放对象实例。
    • 方法区:线程共享,存放已被虚拟机加载的类信息,常量(运行时常量池),静态变量,即时编译器编译后的代码等数据。

    2.2.1 程序计数器

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

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

    • 线程执行的是Java方法: 计数器记录的就是正在执行的虚拟机字节码指令的地址;
    • 线程执行Native方法,计数器为空(Undefined)
      这块内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.

    2.2.2 Java 虚拟机栈

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

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

    • 局部变量表:存放了编译期可知的各种基本数据类型,对象引用和returnAddress类型.其中64位的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个. 局部变量表的内存空间在编译期间完成分配.方法运行期间不会改变局部变量表的大小.
    • 操作数栈
    • 动态链接
    • 方法出口等

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

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

    • StackOverflowError: 线程请求的栈深度大于虚拟机所允许的深度
    • OutOfMemoryError: 如果虚拟机栈可以自动扩展,在扩展时如果没有申请到足够的内存

    2.2.3 本地方法栈

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

    • 虚拟机栈: 虚拟机执行Java方法(字节码)
    • 本地方法栈:虚拟机使用到的Native方法
      本地方法栈也会抛出StackOverflowError和 OutOfMemoryError 异常

    2.2.4 Java 堆

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

    • 按照分代收集算法: 新生代和老年代,
    • 按召收集算法更细致可以分为: Eden空间 / From Survivor空间 和 To Survivor 空间
    • 内存分配: 线程共享的Java堆中可能划分出各个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB).

    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 文件:

    • 类的版本
    • 字段
    • 方法
    • 接口
    • 常量池表
      • 1 编译期生成的各种字面量和符号引用
      • 2 类加载后这些内容会被放到方法区的运行时常量池.
    符号引用(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方法 -> 使用 -> 销毁

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

    • 1 CAS+失败重试 => 原子性
    • 2 Thread Local Alloction Buffer, TLAB 线程私有的本地线程分配缓冲
      虚拟机是否使用TLAB可以使用参数: -XX:+/-UseTLAB

    2.3.2 对象的内存布局

    HotSpot虚拟机中的对象内存:


    对象的内存布局 image.png
    • 1 对象头 Header

      • 1.1 对象自身的运行时数据:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳.
      • 1.2 类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
      • 1.3 如果是数组,还需要记录数组的长度


        image.png
    • 2 实例数据 Instance Data
      实例数据是对象真正存储的有效信息,也就是各种类型的字段内容.
      存储顺序 影响因素:
      - 2.1 虚拟机分配策略参数(HotSpot策略:longs/doubles, ints, shorts/chars, bytes, booleans, oops(Ordinary Object Pointers))
      - 2.2 字段在Java代码中的定义顺序
      - 2.3 满足2.1的顺序,父类中的变量在子类的前面.

    • 3 对齐填充 Padding
      HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,所有有时需要对齐补充来补全.

    2.3.3 对象的访问定位

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

    • 句柄:Java堆中会划分出句柄池, 栈上的reference保存的是对象的句柄地址,句柄包含了对象类型和对象实例各自的信息.
    通过句柄访问对象

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

    • 直接指针: 栈中的reference存的就是对象实例的指针,对象类型的数据保存在对象实例中:
      通过直接指针访问对象

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

    2.4 实战: OutOfMemoryError 异常

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

    本节内容目的:

    • 1 通过代码验证各个区域存储的内容
    • 2 能够debug异常, 知道什么样的代码会导致内存溢出以及解决的办法

    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)

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/ipdxkqtx.html