《深入理解java虚拟机》笔记——第二章 java内存区域

时间:2023-01-01 23:53:40

第二章

  • 2.2 运行时数据区域

    《深入理解java虚拟机》笔记——第二章 java内存区域

    • 程序计数器
      程序计数器(PC)是一块比较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。字节码解释器工作时就是通过这个计数器的值来选取下一条需要执行的字节码指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,称之为“线程私有”的内存。
    • java虚拟机栈
      java虚拟机栈是线程私有的,描述的是java方法执行的内存模型,其实也就是我们俗称的“栈内存”。用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就是一个进栈出栈的过程。局部变量表存放了编译器可知的各种基本数据类型、对象引用。
      如果线程请求的栈深度大于虚拟器所允许的深度,会跑出*Error异常。如果虚拟机栈可以动态扩展,如果扩展无法申请到足够的内存,就会跑出OOM异常。
    • 本地方法栈
      本地方法栈与虚拟机栈作用非常相似,只不过虚拟机栈为jvm执行java方法,而本地方法栈为jvm执行native方法。
    • java堆
      java堆是虚拟机中所管理的内存中最大的一块,唯一目的就是存放对象实例。java堆也是GC管理的主要区域。java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的就可以。
    • 方法区
      方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 运行时常量池
      运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池还具备动态性,java语言并不一定要求常量只在编译期才能产生,也可以在运行期间将新的常量放入池中,例如,String类的intern()方法。当常量池无法再申请内存时会抛出OOM异常。
    • 直接内存
      直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分内存也常常被使用,而且也可能导致OOM。
      在NIO类,引入了一种基于通道(channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样可以显著提高性能,避免了在java堆和Native堆中来回复制数据。
  • 2.3 HotSpot虚拟机对象探秘

    • 对象的创建
      虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那就先执行相应的类加载过程。在类加载检查通过后,就要为新生对象分配内存。内存大小在类加载完后就可以确定。假如java堆是绝对规整的,则使用“指针碰撞”的方式来分配内存。假如java堆是散乱的,那么就用“空闲列表”的方式来分配内存。内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息都存放在对象的对象头综合那个。此时,从虚拟机的角度看,已经产生了一个新的对象。但是从java程序来看,创建才刚刚开始,<init>方法还没有执行,所有字段还都为零。所以执行完new指令后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样才产生了一个真正的对象。

    • 对象的内存布局
      对象在内存中存储的布局分为3块区域:对象头、实例数据、对齐填充。

      • 对象头包括两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据在32位和64位的虚拟机中分别为32bit和64bit,官方称之为“Mark Word”。在Mark Word的32bit空间中的25bit用于存放对象哈希码,4bit用于存放对象分代年龄,2bit用于存储锁标志位,1bit固定为0。
        对象头的另一部分为类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,那么在对象头中还要有一块用于记录数组长度的数据。

      • 实例数据
        实例数据是对象真正存储的有效信息,也是在程序代码所定义的各种类型的字段的内容。

      • 对其填充
        对其填充并不是必然存在的,仅仅是起着占位符的作用,由于JVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象大小必须是8的整数倍,对象头正好是8的整数倍,但是实例数据可能不是8的整数倍,所以就需要通过对其填充来补全。
    • 对象的访问定位
      由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义如何去定位、访问堆中对象的具体地址,所以对象访问方式由JVM决定,现在主流的访问方式分为使用句柄和直接指针。

      • 使用句柄
        java堆中会划分出一块内存作为句柄池,reference中存放的是句柄池的地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
        《深入理解java虚拟机》笔记——第二章 java内存区域

      • 使用直接指针
        那么java堆中的布局中就必须考虑如何放置类型数据的相关信息,而reference中存储的直接就是对象地址。
        《深入理解java虚拟机》笔记——第二章 java内存区域

      这两种对象访问方式各有优势,使用句柄的最大好处就是reference中存储的是稳定的句柄地址,当对象被移动的时候,只改变句柄中的实例数据指针,而reference不需要改变。而使用直接指针的方式最大的好处就是速度更快,因为少了一次指针定位的时间。现在Sun HotSpot是使用第二种方式来进行对象访问。