Java JVM运行时内存区域-简解

Java JVM运行时内存区域-简解,第1张

Java JVM运行时内存区域-简解 Java 运行时数据内存区域

我们首先给出Java内存区域的图解 ,分别给出JDK1.8之前和之后的进行对比。(图片来自网络)

JDK1.8之前:

JDK1.8之后:

通过上面的图解我们可以发现,JDK1.8前后的JAVA虚拟机的数据区域是有改动的。最明显的就是将方法区移除了,取而代之的是在本地内存中的元空间。

而且通过图解我们可以知道:

在一个进程中属于线程私有的是:

  • 程序计数器
  • 本地方法栈
  • 虚拟机栈

属于线程共享的是:

  • 方法区(元空间)
  • 直接内存

接下来我们分别对这些进行介绍:

程序计数器

程序计数器两个作用:

  • 多线程环境下当被重新切换回来在进行恢复现场的时候可以确保程序继续执行未完成的部分
  • 实现代码流程控制,指引程序进行下一步,通俗一点来说就是程序计数器相当于一个指针,当完成一个指令后再指向下一个指令。

通过理解程序计数器的作用,我们就可以知道程序计数器为什么是独立的了,因为它要确保每一个线程能够独立运行而不受其它线程的影响。最后我们需要注意的是程序计数器的生命周期是随着线程的创建而开始的,随着线程的死亡而结束,并且程序计数器是唯一一个不会发生内存溢出的内存区域。

Java虚拟机栈

Java栈的作用:主管Java程序的运行,保存方法的局部变量,部分结果以及参与方法的调用和返回。

我们需要注意的是每次方法的调用在栈中都伴随着一次栈帧的压入,而方法的结束则伴随着栈帧的出栈行为。并且虚拟机栈不存在垃圾回收。

栈帧中都拥有:局部变量表、 *** 作数栈、动态链接、方法出口信息。

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

栈帧的出栈可以是当前方法进行了返回也就是return,也可以是发生了异常。

在Java虚拟机规范中是允许虚拟机栈的大小可变和固定的。所以在出现异常时会有两种不同的报错。

  • StackOverFlowError 我管它叫栈的越界错误。也就是说当固定栈大小时出现请求栈的深度大于当前虚拟机栈的深度时就会报这个错。
  • OutOfMemoryError 这个就是内存不够了。也就是说当允许栈动态大小时,由于系统内存不够导致栈申请扩容时失败就会报这个错。

Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

本地方法栈

本地方法栈的作用与Java虚拟机栈的功能十分相似。在hotspot虚拟机中是与Java虚拟机合二为一的。区别在于本地方法栈是为native方法服务的。其余的都和虚拟机栈类似不再重复。

堆是Java虚拟机管理的最大的一块内存区域,在虚拟机启动时创建并且被进程内的所有线程共享。

Java中的几乎所有的对象实例和数组都在这里进行内存分配。而堆的唯一目的也是存放对象实例。

上面为什么要说“几乎”呢?这是因为随着逃逸分析技术的发展,只要是未逃逸的引用都可以在栈中进行分配。

未逃逸 :就是说在某些方法中该引用未在外部进行引用和或者说未被返回,那么就称该对象未逃逸。

Java堆作为垃圾收集器管理的主要区域也被称为GC堆。所以为了更好的回收内存或者说更快的分配内存,将堆分为了新生代和老年代。JDK1.7之前在HotSpot虚拟机中还存在着一个永久代,永久代就是方法区的实现,不过在JDK1.7之后就被移除了,取而代之的是元空间。

堆中最容易出现的报错就是 OutOfMemoryError 也就是内存溢出。

方法区

方法区是用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,并且被线程所共享。在逻辑上方法区被划分为堆的一部分。

我们知道在JDK1.8之后方法区就被移除了,取而代之的是元空间。我们现在来分析一下为什么要进行替换。

其实在JDK6的时候HotSpot团队就有了将永久代放弃而采用本地内存来实现方法区的规划了,所以在JDK7的时候将字符串常量池和静态变量等移出了放在了堆中。但这时候方法区中的其它东西仍存在在方法区中。在JDK1.8时团队完全放弃了永久代的概念,改用了元空间来代替,并且将方法区中剩余的内容都移到了元空间中。

至于为什么要这么做,那是因为采用直接内存有以下几点好处:

  • 用户如果没有对元空间的大小进行设置,那么元空间的最大大小只受系统内存的限制。这样就减少了内存溢出的风险。
  • 元空间中存放的是类的元数据,这样加载多少类的元数据就不由MaxPerSize控制了,而由系统的实际可用空间来控制。
  • JDK8,合并了HotSpot和JRockit的代码时,由于JRockit中并没有永久代的概念,所以合并之后没有必要单独设置一个永久代了。
运行时常量池

作为方法区的一部分。常量池表被包含在Class文件中。由于是属于方法区的一部分,所以会受到方法区内存的限制。当无法从方法区申请到内存时将会报错OutOfMemoryError 。

这里需要注意的是在JDK1.7之前常量池还包含字符串常量被存放在方法区中(HotSpot中的永久代)。

但是在JDK1.7时字符串常量池就被从方法区中拿出来放到了堆中,在JDK1.8之后HotSpot取消了永久代,而是利用直接内存的元空间替代了永久代。所以除字符串常量池外的剩余的东西也被移到了元空间中,但是字符串常量池则仍放在了堆上。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

Java虚拟机中堆对象的创建

首先我们给出图解:

然后我们分步进行介绍:

  • 类加载检查

    这个类加载检查是在我们使用new 时他会去检查我们要加载的对象是否是合法的,是否存在在常量池中,然后再去查看它是否已经被加载过,初始化过或者解析过,如果没有则启动相应类加载过程。

  • 分配内存

    类加载完,我们就需要为这个对象分配内存,这个分配内存有两种方法:1、指针碰撞 2、空闲列表。使用哪种方法取决于我们的堆内存是否规整。而我们的堆内存是否规整就得看是使用啥垃圾回收机制了,带有规整功能的算法进行垃圾回收后就会使得堆内存变得相对规整。垃圾回收且看后期分解!

    不过我们现在可以来看看对于内存分配这两种方法的一个解析。

    首先我们来讲讲 指针碰撞 所谓指针碰撞其实就是一个分割指针将已分配的内存和未分配的内存区分隔开了,每次请求内存分配时就将指针向未内配的那边移动需要分配的内存大小,那么移动的这块内存就分配给请求内存的对象。这个方法是规整的。(规整也可以叫压缩)。

    然后就是 空闲列表 所谓空闲列表就是将整个堆内存分成大小不一的块,然后维护一个列表来标记那些可用的块。当有请求分配内存时就将大小合适的块分配出去。

    在多线程下我们必须保证线程安全,因为一块内存不可能同时被两个对象分去,那我们是怎么解决这个问题的呢?其实我们是利用了乐观锁CAS配上失败重试、TLAB两种方法。

    • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项 *** 作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新 *** 作的原子性。
    • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
  • 初始化零值

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

  • 设置对象头

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

  • 执行init方法

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

Java对象的内存布局

在HotSpot虚拟机中,对象的布局有三部分 :对象头、实例数据和填充部分。

这里的填充部分没有实际意义,主要是为了占位使得满足对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象头又可以分为两个部分 :第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

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

Java 对象定位方式

Java对象的定位方式有两种

  • 句柄定位

    如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

  • 直接指针

    如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址

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

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/zaji/5692650.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存