JVM深入理解之Java内存模型

本文深入探讨了JVM的运行时数据区域,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区等。详细阐述了各区域的作用、异常情况以及对象的创建过程,如内存分配、初始化和对象结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、JVM运行时数据区域

JVM运行时内存数据区有:方法区、堆、虚拟机栈、本地方法栈、程序计数器。其中方法区和堆属于多个线程共有区域,而其他为每个线程独有。
运行时数据区如下:
JVM运行时数据区
1. 程序计数器:程序计数器是一个比较小的内存区域,主要用来存储线程运行时当前执行的字节码信息。字节码工作时会通过读取程序计数器中待执行字节码的行号来选取下一条要执行的字节码指令。Java虚拟机的多线程通过多个线程轮流切换并分配处理器(内核)时间来执行。一个线程一个处理器(内核),因此,为了切换线程后可以恢复到正确执行位置,每个线程都需要一个计数器,互不影响,相互独立,线程私有。如果线程正在执行一个Java方法,那么程序计数器记录的是正在执行的虚拟机字节码指令地址。如果线程执行一个native方法,因为native方法属于操作系统进行管理了,所以程序计数器为空值。Java中这个区域不会发生OutOfMemoryError异常。

2.虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型,每个放在在执行的时候都会创建一个栈帧用于存储局部变量表,动态链接,方法出口等信息。每个方法从调用到执行完成的过程就对应一个栈帧在虚拟机中入栈和出栈的过程。局部变量表存放了编译器可知的基本数据类型、引用类型和returnAddress类型。其中64位长度的long和double类型会占用两个单位的局部变量空间,其余数据类型占一个。因为局部变量表在编译期间完成内存分配,所以当进入一个方法时,要在帧中分配多大的局部变量空间是完全确定的。
  异常情况:当这个区域所请求的栈深度大于虚拟机所允许的深度,那么将会发生栈异常(抛出StackOverflowError),另外一种情况是初次请求的栈深度小于虚拟机所允许的深度,如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够的内存,那么将会发生(OutOfMemeryError)异常。

3.本地方法栈:像虚拟机栈那样,本地方法栈是为native方法提供服务。

4.堆:Java堆所有线程共享的物理上不一定连续,逻辑上连续的内存数组。此内存区域存放的是对象实例和数组。
  垃圾回收主要管理的是Java堆,现在收集器基本上采用分代收集算法,所以堆可以分为新生代和老年代。再细致点还有Eden空间、From Survivor空间、To Suivivor空间。如果堆中没有内存完成实例分配,并且也不能扩展时,将会发生(OutOfMemeryError)异常。

5.方法区:是各个线程共享区域。用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。HotSpot的垃圾回收器可以像管理Java堆一样管理这部分区域。垃圾收集行为在这个区域比较少见,内存回收主要目的是对常量池的回收以及类型卸载,但是回收条件非常苛刻。当方法区无法完成内存分配需求时,会发生(OutOfMemeryError)异常。

6.运行时常量池:Class文件中信息除了类的版本、字段、方法、接口等描述外,还有常量池。常量池用于存放编译期生成的各种字面量和符号引用。这部分内容在类加载后进入方法区的运行时常量池存放。
  Class文件常量池是确定的,但是运行时常量池具有动态性,比如String类的intern()方法会检查字符串池中有无当前值,如果有,则返回字符串池中对象的引用。只有执行字面量的时候,才会执行检查字符串池的相关操作,如果使用new或者+连接且表达式中存在变量,都不会执行检查字符串池相关操作。当运行时常量池无法在方法区申请到足够内存时会发生(OutOfMemeryError)异常。

7.直接内存:JDK1.4中引入了NIO类,就是直接使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。有助于提高性能,避免数据在内存的来回复制。本机直接内存虽然不会受到Java堆大小的限制,但是毕竟会受到本机总内存以及寻址空间的限制,所以当各个内存区域总合大于机身内存是就会发生(OutOfMemeryError异常)。

二、HotSpot虚拟机对象探秘

1.对象创建:当虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在方法区运行时常量池中定位到一个类的符号引用,并且检查符号引用代表的类是否已被加载、解析和初始化过。如果没有会先执行类加载过程。
  当类加载检查通过后,虚拟机要为新生对象在Java堆中分配内存,对象所需内存在类加载完成后即可确定。分配内存有两种方式,分别为指针碰撞空闲列表指针碰撞假设Java堆已分配内存和未分配内存分别在两边,中间放着一个指针作为分界,所分配内存大小仅仅就是将指针向空闲那边移动对象大小相等的距离。虚拟机初始运行状态下,内存空间相对规整,所以可以采用这种方式,但是当有对象被回收,内存就会变得不规整,这种情况下,除非没回收对象后,虚拟机会对内存进行压缩整理。这种分配方式还会存在一个问题,就是当多个线程同时请求分配内存时,产生的同步问题。实际上虚拟机采用的是CAS(Compare And Swap)配上失败重试的方式保证更新操作的原子性,即先比较版本号,不是最新的版本号,那么失败,将会进行重试。还有一种方式保证同步就是本地线程分配缓冲(TLAB),意思是每个线程都有一个(TLAB),当线程要请求分配内存时,先在TLAB上分配,只有放TLAB用完并分配新的TLAB时才需要同步。空闲列表是当Java堆内存不是那么规整时使用的方式,虚拟机必须维护一个列表,记录那些内存块是可用的,再分配内存的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表。
  当内存分配完毕后,虚拟机需要将分配到的内存都初始化为0,接下来就是虚拟机对对象的一些设置了,比如这个对象是那个类的实例,指向元数据的指针、GC分代年龄,对象哈希码等信息。

2.对象结构:当虚拟机为对象分配内存之后,实际上还没有执行对象的init方法,也就是没有初始化对象内存结构。堆中一个对象内存布局分为对象头,实例数据、对齐填充三部分。
  对象头包含两部分信息,一部分信息存储对象运行时数据,另一部分存储类型指针,指向元数据类型,也就是方法区常量池符号引用。
  实例数据包含实例对象的字段信息,字段信息先从父类继承,然后加上自身字段。一般情况下字节相同的字段被放在一起。
  对齐填充主要是将对象大小调整为8字节的整倍数。

3.对象访问定位:当创建了对象之后,目前主流访问对象方式有句柄和直接指针两种方式。
  使用句柄访问的话,那么Java堆中一部分内存用来作为句柄池,句柄中包含了对象实例数据和类型数据各自的具体地址信息。这种方式reference中存放的就是句柄的地址,所以无论GC怎么压缩整理实例对象地址,只会更新句柄中的信息,reference中的信息并不会改变,但是这种方式会造成可观的开销,如果Java中会创建很多对象。如下图
这里写图片描述
  直接指针访问方式,Reference中存储的直接就是对象实例地址。相比句柄方式,这种方式更快,因为只要一次寻址就行。Sun HotSpot虚拟机就是采用直接指针方式进行对象访问的。
这里写图片描述

参考(JVM深入理解一书),读书笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值