Skip to content

Commit

Permalink
Update Blog content
Browse files Browse the repository at this point in the history
  • Loading branch information
yanggl committed Mar 2, 2024
1 parent be1d3fa commit b2a6d9d
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 8 deletions.
82 changes: 74 additions & 8 deletions src/note/java/jvm/2311081011.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,86 @@ order: 4
date: 2023-11-08
index: true
category:

- Java

tag:

- JVM
---

# JVM内存模型详解

JMM(Java Memory Model)是JVM的一种规范,定义了JVM的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多。
# 运行时数据区详解(内存模型)

JVM运行时数据区数Java虚拟机在运行时对该Java进程占用的内存进行的一种逻辑上的划分,其中包含:**方法区、堆内存、虚拟机栈、本地方法栈、程序计数器**
<!-- more -->

它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。
## 方法区(Method Area)

方法区是线程间共享的区域,在JVM启动时创建,用于存储已经被虚拟机加载的**类信息、常量、静态变量、即时编译后的代码**等数据。

方法区可以被实现成大小固定或可动态扩展和收缩,如果内存空间不满足内存分配要求就会抛出**OutOfMemoryError**异常。

对于HotSpot虚拟机而言,在JDK 1.8以前,方法区被实现为 **“永久代”**(Permanent Generation),属于堆的逻辑组成部分,并提供了两个参数调节其大小,**-XX:PermSize用于设定初始容量,-XX:MaxPermSize用于设定最大容量**。JDK 1.8之后,HotSpot不再有“永久代”的概念,类的元信息数据迁移到被称为“元空间”(Metaspace)的新区域,而静态变量、常量等则存储于堆中。元空间没有使用堆内存,而是分配在 **本地内存(直接内存)** 中,默认情况下其容量只受可用的本地内存大小限制。类似地,HotShot虚拟机也提供了两个参数来调节其大小, **-XX:MetaspaceSize用于设定初始容量,-XX:MaxMetaspaceSize用于设定最大容量**

### 运行时常量池(Runtime Constant Pool)

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

字面值常量:

- 字符串字面量
- 用final修饰的基础类型成员变量的字面值
- 由字面值常量相加得到的结果

### 直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,因此直接内存的分配不受Java堆大小的限制,但是还是会受到本机总内存(RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。

直接内存主要用于NIO类库,实现基于通道(Channel)和缓冲区(Buffer)的IO方式。通常,当需要处理大量数据的读写操作时,可以考虑使用直接内存,例如文件传输、网络编程等。

直接内存优点在于可以避免在Java堆和本地堆之间复制数据,提高IO操作的性能,缺点就是分配和释放代价较高,而且不受JVM垃圾回收的管理,容易造成内存溢出。

直接内存可以通过ByteBuffer类的allocateDirect方法来分配和操作,

## 堆内存(Heap)

堆内存是Java虚拟机中内存最大的一块,也是被所有线程共享的,在虚拟机启动时创建,Java对唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配。

## 虚拟机栈(Virtual Machine Stack)

虚拟机栈(线程栈)描述的是Java方法执行的内存模型,是线程私有的,它的生命周期与线程相同。当每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储**局部变量表、操作数栈、动态链接、方法出口**等信息。

栈帧在线程栈中属于先进后出(FILO),每个方法从调用知道执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

### 局部变量表(LVT)

局部变量表是一个索引以0开始的字节数组,存储了一个方法的所有入参和局部变量。LVT所存储的类型都是编译期可知的,包括各**基础类型(byte、char、short、int、long、float、double、boolean)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)**

LVT有如下特点:

- 第0个Slot(槽位)固定存储指向方法所属对象的this指针
- 除了long和double占用连续两个Slot之外,其余类型只占用一个Slot
- LVT按照变量的声明顺序进行存储

### 操作数栈(OS)

操作数栈用于在方法运算过程存储其中间的运算结果、方法入参和返回结果,它是一个后进先出(Last-In-First-Out,LIFO)的队列。

JVM提供了对OS出栈和入栈的指令,如load指令属于入栈指令、store指令属于出栈指令。

### 动态链接

每个栈帧内都包含一个指向当前方法所属类的运行时常量池引用,也称为符号引用(Symbolic Reference),用于在类加载阶段对代码进行动态链接。动态链接所做的就是根据符号引用所表示名字,转换成对方法或变量的实际引用,从而实现运行时绑定(Late Binding)。

## 本地方法栈(Native Method Stack)

本地方法栈的作用与Java虚拟机栈类似,区别在于后者是为Java方法服务,而本地方法栈则为native方法服务。Java虚拟机规范没有对native方法机制及其实现语言做强制规定,如果JVM不提供native方法,则无需实现本地方法栈。

本地方法栈既可以被实现成固定大小,也可以实现成可动态地扩展和收缩,因此在特定的场景下也会抛出StackOverflowError异常和OutOfMemoryError异常。

## 程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

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

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。


JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证JVM的高效稳定运行。
10 changes: 10 additions & 0 deletions src/note/java/jvm/2311091006.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ KlassPointer又称类型指针,是指向对象的类元数据的指针。虚
在32位的JVM内,类型指针占4byte
在64位的JVM内,类型指针正常占8byte,若开启**指针压缩**或者**最大堆内存小于32G**时占4byte。JDK8后默认开启指针压缩

进行指针压缩的原因:

- 在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据会**占用较大宽带,同时GC也会承受较大压力**
- JVM中32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得JVM只用32位地址就可以支持更大的内存配置(小于等于32G)

需要注意的是:

- 堆内存小于4G时不需要开启指针压缩,JVM会直接去除32位地址,即使用低虚拟地址空间
- 堆内存大于32G时,压缩指针会失效,会强制使用64(8字节)来对Java对象进行寻址,所以堆内存最好不要大于32G。

### 数组长度

如果这个对象是一个数组,则对象头中会有一块4byte长度数数据区用于记录数组的长度,如果不是数组则这部分长度为0
Expand Down
70 changes: 70 additions & 0 deletions src/note/java/jvm/2403021055.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
isOriginal: true
order: 8
date: 2024-03-02
index: true
category:
- Java
tag:
- JVM
---

# 对象创建与内存分配

当我们在Java中创建一个对象时,JVM会执行一系列步骤来完成对象的创建和初始化。本文则记录对象在JVM中完整的创建流程。
<!-- more -->

当我们去new一个对象的时候,完整的流程如下:**类加载检查、内存分配、初始化、设置对象头、执行init()**

## 类加载检查

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

new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。

## 内存分配

类加载完成之后,会为新对象进行内存分配。对象所需的内存大小在类加载完成之后便可以确定。此时为对象分配内存空间等于把一块确定大小的内存从JVM堆中划分出来给对象使用。

在进行堆内存划分的时候会存在两个问题:**如何划分问题和并发问题**

### 内存划分方式

为新对象在堆中划分内存,划分方式有两种:**指针碰撞(默认使用)和空闲列表**

#### 指针碰撞(Bump the Pointer)

通过一个指针指向分界点,这个指针表示**已分配****未分配**内存的分界。当需要分配内存时,将指针往空闲一侧移动与对象大小相等的距离即可。

需要注意的是这需要Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边。主要用于**Serial和ParNew**等不会产生内存碎片的垃圾收集器。

#### 空闲列表(Free List)

JVM维护一张内存列表,记录可用的内存块信息。当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。

空闲列表即使是Java堆中的内存并不规整,已分配内存和未分配内存交错也是适用的。最常见使用此方案的垃圾收集器是**CMS(Concurrent
Mark-Sweep)**

### 并发问题处理

在并发情况对象在内存划分时,可能会出现正在给A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,导致数据不一致或内存分配错误。

JVM采用以下方法来解决并发问题:**CMS和本地线程分配缓冲(TLAB)**

#### CMS(Compare and Swap)

虚拟机采用**CAS配上失败重试**的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

#### 本地线程分配缓冲(Thread Local Allocation Buffer)

为每个线程在Java堆中预先分配一小块内存,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。

虚拟机是否使用TLAB,可以通过 **-XX:+/-UseTLAB(默认开启)** 参数来设定。

### 栈上分配

通常Java对象都是在堆上分配内存,当对象没有被引用时,光依靠GC进行回收内存,对象创建过多就会给GC带来较大的压力,间接影响应用的性能。

为了减少临时对象在堆内存分配的数量,JVM通过**逃逸分析**技术确定对象不会被外部访问,则说明对象不会逃逸出改栈帧,可以将对象在**栈上分配**内存,这样对象占用的内存就会随着栈帧出栈而销毁,达到减轻GC回收的压力。

逃逸分析技术还需要结合**热点探测技术**,以此为前提,详情可在 **[JIT(即时编译)深入理解](./2311221518.md)** 文章查看。

0 comments on commit b2a6d9d

Please sign in to comment.