深入理解Java虚拟机 - 第2章 Java内存区域与内存溢出异常


学习目的:了解虚拟机如何管理内存,能够处理内存泄漏和溢出方面的问题。

2.2 运行时数据区域

Java虚拟机在执行Java程序时,会把它所管理的内存划分为若干个不同的数据区域。按照线程共享或隔离分为两类:一、线程共享的数据区:1. 方法区;2. ;二、线程隔离的数据区:3. 虚拟机栈;4. 本地方法栈;5. 程序计数器

2.2.1 程序计数器

程序计数器(Program Counter Register)】,占较小的内存空间,是当前线程所执行的字节码的行号指示器,是程序控制流的指示器。

每条线程拥有一个独立的程序计数器(线程私有)。原因:Java虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的,在某一时刻,一个处理器都只会指向一条线程中的指令,为了线程切换后能恢复到正确的执行位置,程序计数器线程隔离。

如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)

程序计数器,是唯一一个在《Java虚拟机规范》中没有规定如何OutOfMemoryError的区域

2.2.2 Java虚拟机栈

【Java虚拟机栈】,线程私有,即生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

【局部变量表】存放编译期可知的各种Java虚拟机基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。
局部变量表最基本的存储单元是【Slot】(变量槽),32位长度以内的类型的数据只占用1个Slot,64位的类型的数据(long和double)占用2个。

  • Java虚拟机栈内存不足的两种异常状况:
    1. StackOverflowError:当线程申请的栈深度大于虚拟机所允许的深度时被抛出
    2. OutOfMemeryError:JAVA虚拟机栈容量可以动态扩展时,方法申请的栈大小超过可用内存时被抛出
2.2.3 本地方法栈

本地方法栈作用类似Java虚拟机栈,区别是本地方法栈为本地方法服务,Java虚拟机栈为Java方法(字节码)服务。

2.2.4 堆

【Java堆】是虚拟机所管理的内存最大的一块,在虚拟机启动时创建,Java堆的唯一目的是存放对象实例。

Java堆也叫GC堆,是垃圾收集器管理的内存区域。

如何提高对象分配时的效率:把所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区

Java堆可以处在物理上不连续的内存空间,但逻辑上必须视为连续。Java堆可拓展(通过设置参数-Xmx和-Xms)。

当Java堆中没有内存完成实例分配,而且堆也无法拓展时,Java虚拟机抛出OutOfMemeryError异常。

2.2.5 方法区

方法区,别名“非堆(Non-Heap)”,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区的垃圾回收主要针对常量池的回收和类型的卸载。

方法区无法满足新的内存分配需求时,抛出OutOfMemeryError异常。

2.2.6 运行时常量池

【运行时常量池(Runtime Constant Pool)】是方法区的一部分

【Class文件常量池】,先介绍每个class文件都有一个常量池,用于存储字符串常量、类和接口名字、字段名、和其他一些在class中引用的常量。

【运行时常量池】存储两种类型,分别是从class文件常量池构建的static constants静态常量和symbolic references符号引用。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,可以在运行期间将新的常量放入池中。

【字符串常量池】保存的是“字符”的实例,供运行时常量池引用。(在JDK8以后字符串常量池位于堆中

2.2.7 直接内存

直接内存Direct Memory并不是虚拟机运行时数据区的一部分

在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

直接内存出现 OutOfMemoryError 异常的原因是物理机器的内存是受限的,但是我们通常会忘记需要为直接内存在物理机中预留相关内存空间。

2.3 HopSpot虚拟机对象探秘

以HotSpot和Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程

2.3.1 对象的创建
  • 接下来讨论的是用new关键字创建普通Java对象(不包括数组和Class对象等),过程分为①类加载检查、②分配内存、③初始化和④设置对象头信息

    1. 类加载检查。当Java虚拟机遇到字节码new时,先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,则先执行相应的类加载过程。(本书第七章探讨)
    2. 分配内存。Java虚拟机为新生对象分配内存。类加载后便可确定对象所需内存的大小。分配内存的两种方法:(1)指针碰撞:前提是内存规整,使用过的内存放在一边,空闲的内存另一边,中间放着一个指针作为分界点的指示器,那分配内存操作简单,把指针向空闲内存方向移动与对象大小相等的长度。(2)空闲列表:前提:Java堆中内存不规整,虚拟机维护一个列表,记录哪些存储块是可用的,在分配时找到一块足够大的空间划分给对象实例,并更新列表上的记录。(Java堆是否规整取决于所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力)
      分配内存中划分空间时要考虑线程安全,并发时可能在给A分配内存,指针没来得及修改,对象B又同时使用了原来的指针分配内存。解决方案:(1)对分配内存空间的动作进行同步处理,采用CAS配上重试的方式保证更新操作的原子性(2)把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。(虚拟机是否使用TLAB,通过-XX:+/-UseTLAB参数设定)
    3. 初始化。内存分配完成后,虚拟机必须将分配到的内存空间(不包括对象头)都初始化为零值。(如果使用了TLAB,这一项操作也可以提前至TLAB分配时进行)
    4. 设置对象头信息,例如:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的GC分代年龄等信息,这些信息存放在**对象头(Object Head)**中。
  • <init>()方法。
    经历了①类加载检查 -> ②分配内存 -> ③初始化 -> ④设置对象头后,从虚拟机视角,一个新的对象已经产生,但从程序的视角来看,对象创建才刚开始——构造函数,即Class文件中的<init>方法还没执行,所有字段都是默认的零值。一般来说,new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

2.3.2 对象的内存布局
  • 在HotSpot虚拟机中,对象在堆内存中的存储布局划分为三个部分:
    1. 对象头(Header)
      • 对象头包括两类信息:Mark Word类型指针

      • 第一类是Mark Word,用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁。这部分信息的长度在32和64位的虚拟机(未开启指针压缩)中分别为32bit和64bit。
        对象要存储的数据很多,其实已经超过32bit或64bit,但对象头的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效益,Mark Word被设计成是一个有动态定义的数据结构,以便节省空间。例如下表
        在这里插入图片描述

      • 第二类是类型指针,即对象指向它的类型元数据的指针。JVM通过类型指针确定对象是哪个类的实例。(因为查找对象的元数据信息不一定要通过对象本身,所以并不是所有的JVM实现都会在对象数据上保留类型指针)
        如果对象是一个Java数组,那在数组头还必须有一块用于记录数组长度的数据。

    2. 实例数据(Instance Data)。
      • 定义:实例数据是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容
      • 这部分内容存储①依照类型定义顺序,所以相同宽度的字段总是会被分配到一起存放
      • 父类定义的变量会出现在子类之前。
    3. 对齐填充(Padding),不是必须存在的,仅仅作占位符。
      • 原因:补全对象的长度为8字节的整数倍
      • 因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,所以任何对象的大小都必须是8字节的整数倍。因此,对象头的长度已经被精心设计为8字节的1倍(32bit)或2倍(64bit);因此,如果对象实例数据部分没有对齐的话,需要通过对其填充来补全。
2.3.3 对象的访问定位
  • 对象访问指从栈的reference数据找到堆上的具体对象。主流的访问方式有①使用句柄②直接指针两种。(HotSpot虚拟机采用速度更快的直接指针方法)

    1. 使用句柄,Java堆中需要划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据的地址信息。
      优势:reference中存储的是稳定句柄地址,当对象被移动(GC时经常移动对象)时只改变句柄中的实例数据指针,而不用改变reference本身。
      在这里插入图片描述

    2. 直接指针,reference中存储的是对象实例数据的地址,关于如何放置访问类型数据的相关信息需要java堆中对象的内存布局时来考虑。如果只是访问对象本身的话,就不需要多一次间接访问的开销。
      优势:速度更快。节省了一次指针定位的时间开销。(Java中对象访问很频繁)
      在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值