1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

Update Java内存区域.md

This commit is contained in:
guide 2021-04-25 15:09:40 +08:00
parent b0cfa99bf8
commit 3623668462

View File

@ -56,6 +56,7 @@
对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为每一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
## 二 运行时数据区域
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。
**JDK 1.8 之前:**
@ -66,7 +67,6 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成
![](./pictures/java内存区域/Java运行时数据区域JDK1.8.png)
**线程私有的:**
- 程序计数器
@ -80,6 +80,7 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成
- 直接内存 (非运行时数据区的一部分)
### 2.1 程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。**
另外,**为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。**
@ -171,8 +172,6 @@ JDK 8 版本之后方法区HotSpot 的永久代被彻底移除了JDK1.7
> }
>
> ```
>
>
堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:
@ -219,6 +218,7 @@ JDK 1.8 的时候方法区HotSpot 的永久代被彻底移除了JDK1
![](https://img-blog.csdnimg.cn/20210425134508117.png)
1. 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
> 当元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace`
你可以使用 `-XXMaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited这意味着它只受系统内存的限制。`-XXMetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
@ -240,12 +240,9 @@ JDK 1.8 的时候方法区HotSpot 的永久代被彻底移除了JDK1
> 1. **JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代**
> 2. **JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代**
> 3. **JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)**
>
相关问题JVM 常量池中存储的是对象还是引用呢?: https://www.zhihu.com/question/57109429/answer/151717241 by RednaxelaFX
### 2.7 直接内存
**直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。**
@ -254,11 +251,12 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
## 三 HotSpot 虚拟机对象探秘
通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。
### 3.1 对象的创建
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
![Java创建对象的过程](./pictures/java内存区域/Java创建对象的过程.png)
@ -270,7 +268,6 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。
**内存分配的两种方式:(补充内容,需要掌握)**
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
@ -296,7 +293,6 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`<init>` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `<init>` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
### 3.2 对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。
@ -308,6 +304,7 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
### 3.3 对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**① 使用句柄**和**② 直接指针**两种:
1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
@ -318,11 +315,8 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
![对象的访问定位-直接指针](./pictures/java内存区域/对象的访问定位-直接指针.png)
**这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。**
## 四 重点补充内容
### 4.1 String 类和常量池
@ -363,11 +357,12 @@ System.out.println(s2);//计算机
System.out.println(s1.equals(s2));//true
System.out.println(s3.equals(s2));//true因为两个都是常量池中的 String 对象
```
`s1.equals(s2)` 输出为 true 的原因 :
1. s1调用intern的时候因为常量池没有对应的字面量所以在常量池保存了一个指向s1的引用
1. s1 调用 `intern()` 的时候,因为常量池没有对应的字面量,所以在常量池保存了一个指向 s1 的引用
2. 接下来的 s2 会先去常量池里找,找到对应引用,故指向堆里的 s1
3. 故 s1==s2 为true
3. 故 `s1.equals(s2)`true
**字符串拼接:**
@ -382,9 +377,11 @@ System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
```
![字符串拼接](./pictures/java内存区域/字符串拼接-常量池2.png)
尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
### 4.2 String s1 = new String("abc");这句话创建了几个字符串对象?
**将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”那么它将首先在池中创建然后在堆空间中创建因此将创建总共 2 个字符串对象。**
@ -456,6 +453,7 @@ private static class CharacterCache {
```
**应用场景:**
1. Integer i1=40Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
2. Integer i1 = new Integer(40);这种情况下会创建新的对象。
@ -464,6 +462,7 @@ private static class CharacterCache {
Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出 false
```
**Integer 比较更丰富的一个例子:**
```java