Chapter2-Java内存区域与内存溢出异常

  • 2.1 概述
    • 由于Java虚拟机拥有自动内存管理机制,不易出现内存泄漏和内存溢出。然而,正因如此,需要了解虚拟机是怎样使用内存的,才能排查错误、修正问题。
  • 2.2 运行时数据区域
    • Java虚拟机在执行Java程序时会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间。

    • 2.21 程序计数器
      • 程序计数器(Program Counter Register)是一块较小的内存空间,通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器。
      • 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,这类内存区域为“线程私有”的内存。
      • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
    • 2.22 Java虚拟机栈
      • Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。
      • 每个方法被执行的时候,JVM都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
      • 局部变量表存放了编译期可知的基本数据类型,对象引用和returnAddress类型。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示。
      • 两类异常状况:
        • 如果线程请求的栈过深,将抛出StackOverflowError异常;
        • 如果栈容量动态扩展后,内存不足会抛出OutOfMemoryError异常。
    • 2.23 本地方法栈
      • 本地方法栈(Native Method Stacks)与虚拟机栈作用相似,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
    • 2.24 Java堆
      • Java堆(Java Heap)是虚拟机内存中最大的一块,被所有线程共享。所有的对象实例以及数组都应当在堆上分配,Java堆可处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
      • 所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。
      • 如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
    • 2.25 方法区
      • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,也叫“非堆”(Non-Heap)
      • JDK 8完全废弃了永久代的概念,改用元空间(Metaspace)。相对而言,垃圾收集行为在这个区域的确是比较少出现的。
      • 会抛出OutOfMemoryError异常
    • 2.26 运行时常量池
      • 运行时常量池(Runtime Constant Pool)是方法区的一部分。常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
      • 运行时常量池相对于Class文件常量池具备动态性,运行期间也可以将新的常量放入池中,例如String类的intern()方法。
      • 会抛出OutOfMemoryError异常
    • 2.27 直接内存
      • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,但也被频繁地使用,可能导致OutOfMemoryError异常出现。
  • 2.3 虚拟机对象揭秘
    • 以最常用的虚拟机HotSpot和最常用的内存区域Java堆为例,深入探讨一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程
    • 2.3.1 对象的创建
      • 1. 当Java虚拟机遇到new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,虚拟机将为对象分配内存,对象所需内存在类加载完成后便可完全确定
      • 2. 分配内存时,假设Java堆中内存是绝对规整的,例如使用Serial、ParNew等带压缩整理过程的收集器时,可使用“指针碰撞”(Bump The Pointer)。反之,如果已被使用的内存和空闲的内存相互交错,例如CMS这种基于清除(Sweep)算法的收集器,必须维护一个“空闲列表”(Free List)。
      • 3. 内存分配完成之后,虚拟机必须将分配到的内存空间都初始化为零值,设置对象头信息
      • 4. 最后执行构造函数,即Class文件中的<init>()方法
    • 2.3.2 对象的内存布局
      • 对象在堆内存中的存储为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
      • HotSpot虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,官方称它为“Mark Word”。另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
      • 实例数据部分是对象真正存储的有效信息,即在代码里面所定义的各种类型的字段内容。
      • 对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
    • 2.3.3 对象的访问定位
      • 1. 句柄访问:Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
      • 2. 直接指针访问:Java堆中对象的内存放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
  • 2.4 实战:OutOfMemoryError异常
    • 2.4.1 Java堆溢出
      • 通过内存映像分析工具对Dump出来的堆转储快照进行分析,先确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(MemoryOverflow)。
      • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们。
      • 如果不是内存泄漏,即内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况。
    • 2.4.2 虚拟机栈和本地方法栈溢出
      • HotSpot虚拟机不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
    • 2.4.3 方法区和运行时常量池溢出
      • JDK7及以后,原本存放在永久代的运⾏时常量池被移⾄Java堆之中。
      • 运行时常量池是方法区的一部分,方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比较苛刻的。
      • JDK6中intern()⽅法会把⾸次出现的字符串实例复制到永久代的字符串常量池中存储,返回永久代中这个字符串实例的引⽤;JDK7中intern()⽅法由于字符串常量池移动⾄堆中,不需要拷⻉字符串实例到永久代,⽽是在常量池中记录其⾸次出现的实例引⽤。
    • 2.4.4 本机直接内存溢出
      • 直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现Dump文件很小,而程序中又使用了DirectMemory),那就可以检查一下直接内存
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值