JVM之Java内存区域与Java对象

本文深入探讨Java内存区域划分,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池的功能与异常处理。同时,详细解析Java对象在虚拟机中的创建流程,内存布局及访问定位方式。

1. Java 内存区域

      Java语言不需要像C语言那样去malloc或者free内存,不容易出现内存泄漏和内存溢出的问题。为了方便JVM进行管理,在程序运行的过程中,JVM会把其管理的内存划分为若干区域。每个区域都有自己的用途,并且创建或销毁的时间也并不都是相同的。

1.1 运行时数据区域

      Java 程序在运行过程中,内存区域会被划分为如下几个区域。

在这里插入图片描述

1.1.1 程序计数器

      在多线程环境下,线程进行切换时,需要保存、恢复其上下文环境(保存可以理解为当前线程执行到哪一行代码、恢复则可以理解为当前线程切换前执行到哪一行代码,现在需要接着那一行代码继续执行)。

      而程序计数器的作用就是记录每一个线程所执行到的字节码位置,如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的字节码指令的地址;如果正在执行的是 Native 方法,则计数器的值为空。

      除了进行上下文切换需要使用到程序计数器,通过更改程序计数器的值,也可以实现代码的流程控制,如:顺序执行、选择、循环、异常处理

      程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。

1.1.2 Java 虚拟机栈

      每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,而且 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程

      局部变量表主要存放了编译器可知的各种数据类型(boolean、 byte、char、 short、int、 float、 long、double)、 对象引用(reference类型,它不同于对象本身,可能是-个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

在这里插入图片描述

      Java虚拟机栈会出现两种异常: StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError:当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常,比如一个无限递归的函数就会引发该异常。

  • OutOfMemoryError:当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常,比如调用一个方法,该方法创建的对象引用始终被引用,导致JVM不能进行回收,并且一直在创建对象,就会引发该异常。

1.1.3 本地方法栈

       和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native 方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一

       本地方法栈也会出现StackOverFlowErrorOutOfMemoryError两种异常。

1.1.4 Java 堆

       Java 堆是JVM中所管理的最大的一块内存,Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存。随着栈逃逸分析的技术,对象不会再一定被分配到堆中,也可以被分配到栈中。这样做的好处就是随着方法的运行结束,对象也会随着出栈而被销毁,不再需要JVM去销毁。

      根据对象被创建和销毁的特点,Java 堆 又被分为新生代和老年代。新生代的特点是,对象的生命周期较短,存活时间不长便被GC回收。而老年代的特点则是对象的生命周期较长,存活时间较长。
在这里插入图片描述

      堆区是被所有线程所共享的内存区域,如果给每个线程在分配内存的时候,需要保证每个线程的内存和其他线程的内存区域不会出现重合。所以,在每次分配内存区域时是通过加锁的方式实现。

      而加锁及释放锁耗费的资源较多,需要在用户态和内核态之间来回切换。为了降低每个线程对于锁资源的竞争,JVM通过TLAB(Thread Local Allocation Buffer)的方式进行内存分配,即给每个线程都分配一块内存,当这个线程的内存使用的差不多了,在通过竞争锁的方式分配内存。这样比通过一个指针记录已分配内存区域和未分配内存区域的方式,可以极大的降低对锁资源的竞争。

      Java堆的大小并不是固定的,可以通过参数-Xmx-Xms设置,如果Java堆的内存没办法再扩展时,则会引发OutOfMemoryError异常。

1.1.5 方法区

      方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等数据。

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

      在JDK 1.8之前,方法区是通过永久代实现的,永久代是一块连续的内存空间,这样做的好处时,不需要专门为方法区写内存管理的代码,但是这种设计导致了Java应用更容易遇到内存溢出的问题,在过去(自定义类加载器还不是很常见的时候),类大多是”static”的,很少被卸载或收集,对于被JVM自带的加载器所加载的类来说,始终有引用指向其Class对象,导致无法回收。当有大量动态生成的Class对象,如JPS或者动态代理所生成的Class对象,极易导致OOM

      而在JDK 1.8中,使用元空间代替永久代,其使用的是本机内存,当本机内存不够时依然会发生OOM,但是OOM的概率已经被下降了很多,往往只取决于本机内存的大小。

1.1.6 运行时常量池

      运行时常量池是方法区的一部分,Class文件中常量池表中的各个编译期字面量和符号引用,将在被类加载后存放到方法区的运行时常量池中。

      运行时常量池相对于Class 文件常量池的另一个重要区别就是具备动态性Java 并不要求常量一定只有编译期才能产生,在运行期也可以将常量存储在运行时常量池中。

      当运行常量池无法申请到新的内存时,将抛出OutOfMemoryError异常。

1.2 直接内存

      直接内存并不是虚拟机运行时数据区,而是本机内存。

      如果我们想修改一个文件,需要涉及到以下几个阶段。数据由磁盘拷贝到内核态缓冲区、内核态缓冲区再拷贝到用户态缓冲区、用户态下对文件进行修改、用户态缓冲区再拷贝到内核态缓冲区、内核态再拷贝到磁盘。历经四次拷贝,才能完成完成对文件的一次写操作。

      而直接内存的作用可以看作时直接开辟一区域,用来记录内核态缓冲区的地址,数据被修改后,可以根据地址拷贝到内核态缓冲区。这样便可以降低拷贝的次数,提高效率。

2. Java对象在虚拟机中的创建与访问定位

2.1 对象的创建

在这里插入图片描述

      ① 类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

      ② 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有”指针碰撞”和"空闲列表”两种,选择那种分配方式由Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
      选择以上两种方式中的哪一种,取决于Java堆内存是否规整。而Java堆内存是否规整,取决于GC收集器的算法是"标记-清除",还是"标记整理" (也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

在这里插入图片描述

      ③初始化零值:内存分配完成后, 虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

      ④设置对象头:初始化零值完成之后, 虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象头中。 另外, 根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

      ⑤执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才 算完全产生出来。

2.2 对象的内存布局

在这里插入图片描述

2.2.1 对象头

      对象头通常分为两部分,一部分用于存储对象自身的运行时数据,称为"Mark Word",另一部分为类型指针,指向对象所属类型元数据的指针。同时,如果对象是数组的话,则对象头还有一部分用于记录数组的长度。

      Mark Word结构如下所示:
在这里插入图片描述

2.2.2 实例数据

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

2.2.3 对齐填充

      对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。因为Hotspot虚拟机的自动内存 管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

2.3 对象的访问定位

      对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄直接指针两种。

2.3.1 句柄方式

在这里插入图片描述

2.3.2 直接指针

在这里插入图片描述
      两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销

      HotSpot虚拟机,主要使用直接指针的方式进行访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值