内存区域

概述

​ 对Java程序员来说,在自动内存管理机制的帮助下,不需要为每个new操作写配对的delete、free代码。但也是由于把权力给了Java虚拟机,一旦内存出现泄露和溢出的问题,若不了解虚拟机如何使用内存,那将成为艰难的工作。这里主要介绍运行时数据区、虚拟机对象、OOM异常。

运行数据区

​ JVM在执行过程中会将它管理的内存区域划分为若干不同数据区域。JVM管理的内存包含一下几个运行时数据区:方法区,堆区,虚拟机栈,本地方法栈,程序计数器。其中前两者是线程

20220414145037

程序计数器

​ 当前线程所执行字节码的行号指示器。在JVM概念模型内,字节码解释器工作时就是通过改变这个指示器的值来选取下一条需要执行的字节码指令,它是控制程序流的指示器,分支、跳转、循环、异常处理、线程恢复都需要依赖计数器来完成。

​ JVM的多线程是通过线程轮询,分配处理机时间来实现的。在一个确定的时间只有一个处理器(多线程为内核)在执行一个线程的指令。为了线程能恢复到正确的执行位置,每个线程都需要独立的程序计数器,称为线程私有。

​ 若线程正在执行的是Java方法,则计数器记录的是正在执行的JVM字节码指令的地址;若执行的是本地(Native)方法,则计数器记录为空(undefined)

虚拟机栈

​ 和计数器一样,同为线程私有。生命周期和线程相同,方法被执行的时候都会创建对应栈帧,其内存储局部变量表等信息。方法被调用到执行完毕,对应该方法栈帧在栈内的出栈和入栈的过程。

​ 局部变量表存放编译期的基本数据类型、引用类型。引用类型并不等同对象本身,而是与对象相关的地址

本地方法栈

​ 为执行Native方法服务

Java堆

lingxing_beijing-001

​ 线程共享的内存区域,目的是存放对象实例。GC收集器管理的区域。从分配内存角度看,堆中可以划分多个分配缓冲期TLAB,提升分配效率。可以处于物理不连续,逻辑上视为连续的内存,堆大小可以通过-Xmx和-Xms配置大小,若没内存进行实例分配也无法拓展时,抛出OOM异常

方法区

​ 线程共享的内存区域,存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

​ 起初虚拟机团队把收集器的分代设计扩展至方法区,用永久代来实现方法区,使得垃圾回收器能管理这部分内存。但这种设计导致了应用更容易遇到内存溢出问题。JDK6时开发团队有放弃永久代,逐步采用本地内存实现方法区的计划,JDK7HotSpot把原本在永久代的字符串常量池、静态变量移出,JDK8完全废弃了永久代概念,该用元空间来代替,把JDK7中剩余的内容移到元空间中

​ 该区域和堆一样不需要连续的内存和选择固定大小或可扩展外,甚至可以不实现垃圾收集器。该区域的内存回收目标主要针对常量池的回收和类型的卸载,一般来说回收效果令人难以满意。

运行时常量池

​ 运行时常量池是方法区的一部分,包含字符串常量池。其内存放的是Class文件中的常量池信息(Constant Pool Table),即编译期生成的各种字面量与符号引用,以及符号引用翻译后的直接引用,会在类加载后存放在池内。Class文件中包含类版本、字段、接口、方法、常量池等信息。

​ 运行时常量池有动态性,常量并非都是编译期产生,运行器产生的新常量也会入池,如String类的intern()方法。

直接内存

​ 直接内存direct Memery并非虚拟机运行时数据区的一部分,但也会被频繁地使用,导致OOM异常。

​ JDK1.4中加入NIO(New IO)类,引入一种基于通道channel与缓冲区的IO方式,可以用Native函数库分配堆外内存,然后通过存储在堆内的DirectByteBuffer对象作为引用操作。这能在一定场景中提升性能,避免在java堆和Native堆中来回复制数据。

HotSpot虚拟机对象

jiwejiu-005

​ 介绍虚拟机内存中数据的细节,如如何创建、如何布局、如何访问。这里以HotSpot虚拟机和Java堆为例,讨论虚拟机在堆中对象分配、布局和访问的过程。

对象创建

​ Java作为面向对象语言,运行时都有对象被创建。在语言层面,创建对象(排除复制、反序列化)仅为new关键字,在JVM层面,对象(排除数组、Class对象)的创建过程。

​ 当JVM碰到一条字节码new指令时,首先检查这个指令的参数能否在常量池中定位到对应的符号引用,若没有则执行类加载过程,即加载、解析、初始化类。

​ 通过类加载检查,开始分配内存,对象实际大小在类加载完成后确定。从内存中划分空间有两种方式:指针碰撞、空闲列表。两者的区别是前者是连续内存,后者是非连续内存。分配方式由Java堆是否规整决定,堆是否规整又取决于垃圾回收器是否带有空间压缩整理功能,像serial、ParNew等便带有,系统则采用指针碰撞,像CMS这种基于Sweep清理的算法,则用空闲列表

​ 内存在分配的时候会有并发问题,解决方案:分配行为同步CAS,按线程划分TLAB。分配空间后需要初始化零值以便不赋值的情况下使用。接下来对对象头进行设置,保存类相关信息。

​ 这是一个对象便有了形状,但还未进行构造。new后会执行init方法,按程序对对象进行初始化,此时一个真正可用的对象诞生了。

对象内存布局

​ 对象在堆内的划分为3个部分:对象头、实例数据、对齐填充

​ 对象头包含两类数据,对象运行时数据及类型指针。运行时数据包含哈希码、GC分代年龄、锁状态标志等,不同位机器对象头长度不同分32字节和64字节,类型指针来确定类的类型,有时对象头也包含数组长度在对象是数组的时候。
202204241829

​ 实例数据存储程序定义的各种类型字段内容,包含父类继承和子类中的。对齐填充非必须,为了满足对象地址必须是8字节整数倍。

对象定位访问

​ Java通过栈内引用对象操作堆上的具体对象,如何实现定位由JVM实现,主流方式有句柄访问和直接指针两种

HotSpot主要是用直接指针访问,两者的区别也是句柄和直接指针的区别。

20220419163800

参考文章

深入了解Java虚拟机

posted @ 2022-04-20 20:42  lifelikeplay  阅读(36)  评论(0编辑  收藏  举报