Administrator
发布于 2023-02-01 / 54 阅读
0
0

JVM内存划分

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

jvm

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储(简单理解为是线程的便签纸,记录的是做到哪一步了,是记住下一条jvm指令的执行地址),这类内存区域为“线程私有”的内存。

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

虚拟机规范对该内存区域规定了两类异常状况:

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

Java堆

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。

ps:注意这里是几乎所有,实际情况是还有栈上分配以及下面介绍的TLAB
https://zhuanlan.zhihu.com/p/357146298

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作GC堆(Garbage CollectedHeap)

Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,但对于大对象(典型的如数组对象),
多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

除了保存Class文件中描述的符号引用外,由符号引用翻译出来的直接引用也存储在运行时常量池中

运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时
会抛出OutOfMemoryError异常。

直接内存

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

JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区
(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的
DirectByteBuffer对象作为这块内存的引用进行操作。

本机直接内存的分配不会受到Java堆大小的限制,但定还是会受到本机总内存(包括物理内存、SWAP分区
或者分页文件)大小以及处理器寻址空间的限制

对象的创建

在编程语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在虚拟机中,当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定 ,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。

两种内存分配方式
1、指针碰撞(Bump ThePointer)
Java堆中的内存是规整的,已使用的在一边,没使用的在另一边,中间有一个指针作为分界点的指示器,
分配内存就是将中间的指针移动一段与所需内存大小相同的距离即可

2、空闲列表(Free List)
堆中的内存是不规整的,已使用的和未使的相互交错,那么虚拟机会维护一个列表,记录上哪些内存块是
可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

选择哪种内存分配方式,得看使用哪种垃圾回收算法。

CMS虽然是基于清除算法,但为了能在多数情况下分配得更快,设计了一个叫作LinearAllocation Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区之后,在它里面仍然可以使用指针碰撞方式来分配。

内存分配的并发问题

上述说会维护一个指针作为分界点,但如果在并发情况下,正在给对象A分配内存,但指针还没来得及修改,此时对象B又使用原来的指针来分配内存,此时会出现线程不安全问题。

对此有两种解决方案
1、分配内存同步处理
采用CAS配上失败重试的方式保证更新操作的原子性

2、分配本地线程缓冲(Thread Local AllocationBuffer,TLAB)
每个线程在Java堆中预先分配一小块内存,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,
只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。是否使用本地线程缓冲可以设置开启

内存分配完成后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值(数据的初始化,默认值)。

Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

以上,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数。即Class文件中的init()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。只有执行完init()方法后,按照程序员的意愿对对象进行了初始化,这样一个真正可用的对象才算完全被构造出来。

对象的内存布局

对象的访问定位

1、句柄访问

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

2、直接指针

reference(引用)中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销

两种对象访问方式各有优势,
使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象
是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销

OutOfMemoryError异常

通过参数-XX:+HeapDumpOnOutOf-MemoryError可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析。


评论