mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-20 22:17:09 +08:00
Update 可能是把Java内存区域讲的最清楚的一篇文章.md
This commit is contained in:
parent
d8ff3cb369
commit
15fc507aff
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [写在前面(常见面试题)](#写在前面常见面试题)
|
- [Java 内存区域详解](#java-内存区域详解)
|
||||||
|
- [写在前面 (常见面试题)](#写在前面-常见面试题)
|
||||||
- [基本问题](#基本问题)
|
- [基本问题](#基本问题)
|
||||||
- [拓展问题](#拓展问题)
|
- [拓展问题](#拓展问题)
|
||||||
- [一 概述](#一-概述)
|
- [一 概述](#一-概述)
|
||||||
@ -11,19 +11,30 @@
|
|||||||
- [2.3 本地方法栈](#23-本地方法栈)
|
- [2.3 本地方法栈](#23-本地方法栈)
|
||||||
- [2.4 堆](#24-堆)
|
- [2.4 堆](#24-堆)
|
||||||
- [2.5 方法区](#25-方法区)
|
- [2.5 方法区](#25-方法区)
|
||||||
|
- [2.5.1 方法区和永久代的关系](#251-方法区和永久代的关系)
|
||||||
|
- [2.5.2 常用参数](#252-常用参数)
|
||||||
|
- [2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?](#253-为什么要将永久代-permgen-替换为元空间-metaspace-呢)
|
||||||
- [2.6 运行时常量池](#26-运行时常量池)
|
- [2.6 运行时常量池](#26-运行时常量池)
|
||||||
- [2.7 直接内存](#27-直接内存)
|
- [2.7 直接内存](#27-直接内存)
|
||||||
- [三 HotSpot 虚拟机对象探秘](#三-hotspot-虚拟机对象探秘)
|
- [三 HotSpot 虚拟机对象探秘](#三-hotspot-虚拟机对象探秘)
|
||||||
- [3.1 对象的创建](#31-对象的创建)
|
- [3.1 对象的创建](#31-对象的创建)
|
||||||
|
- [Step1:类加载检查](#step1类加载检查)
|
||||||
|
- [Step2:分配内存](#step2分配内存)
|
||||||
|
- [Step3:初始化零值](#step3初始化零值)
|
||||||
|
- [Step4:设置对象头](#step4设置对象头)
|
||||||
|
- [Step5:执行 init 方法](#step5执行-init-方法)
|
||||||
- [3.2 对象的内存布局](#32-对象的内存布局)
|
- [3.2 对象的内存布局](#32-对象的内存布局)
|
||||||
- [3.3 对象的访问定位](#33-对象的访问定位)
|
- [3.3 对象的访问定位](#33-对象的访问定位)
|
||||||
- [四 重点补充内容](#四--重点补充内容)
|
- [四 重点补充内容](#四--重点补充内容)
|
||||||
- [String 类和常量池](#string-类和常量池)
|
- [4.1 String 类和常量池](#41-string-类和常量池)
|
||||||
- [String s1 = new String("abc");这句话创建了几个对象?](#string-s1--new-stringabc这句话创建了几个对象)
|
- [4.2 String s1 = new String("abc");这句话创建了几个字符串对象?](#42-string-s1--new-stringabc这句话创建了几个字符串对象)
|
||||||
- [8种基本类型的包装类和常量池](#8种基本类型的包装类和常量池)
|
- [4.3 8 种基本类型的包装类和常量池](#43-8-种基本类型的包装类和常量池)
|
||||||
- [参考](#参考)
|
- [参考](#参考)
|
||||||
|
|
||||||
<!-- /TOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
# Java 内存区域详解
|
||||||
|
|
||||||
## 写在前面 (常见面试题)
|
## 写在前面 (常见面试题)
|
||||||
|
|
||||||
### 基本问题
|
### 基本问题
|
||||||
@ -37,12 +48,10 @@
|
|||||||
- **String 类和常量池**
|
- **String 类和常量池**
|
||||||
- **8 种基本类型的包装类和常量池**
|
- **8 种基本类型的包装类和常量池**
|
||||||
|
|
||||||
|
|
||||||
## 一 概述
|
## 一 概述
|
||||||
|
|
||||||
对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
|
对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。
|
||||||
|
|
||||||
|
|
||||||
## 二 运行时数据区域
|
## 二 运行时数据区域
|
||||||
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。
|
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。
|
||||||
|
|
||||||
@ -70,7 +79,6 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成
|
|||||||
- 方法区
|
- 方法区
|
||||||
- 直接内存 (非运行时数据区的一部分)
|
- 直接内存 (非运行时数据区的一部分)
|
||||||
|
|
||||||
|
|
||||||
### 2.1 程序计数器
|
### 2.1 程序计数器
|
||||||
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。**
|
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。**
|
||||||
|
|
||||||
@ -134,11 +142,11 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC堆(Ga
|
|||||||
|
|
||||||
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。
|
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。
|
||||||
|
|
||||||
#### 方法区和永久代的关系
|
#### 2.5.1 方法区和永久代的关系
|
||||||
|
|
||||||
> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久带这一说法。
|
> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久带这一说法。
|
||||||
|
|
||||||
#### 常用参数
|
#### 2.5.2 常用参数
|
||||||
|
|
||||||
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
|
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
|
||||||
|
|
||||||
@ -160,7 +168,7 @@ JDK 1.8 的时候,方法区(HotSpot的永久代)被彻底移除了(JDK1.
|
|||||||
|
|
||||||
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
|
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
|
||||||
|
|
||||||
#### 为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
|
#### 2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
|
||||||
|
|
||||||
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
|
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
|
||||||
|
|
||||||
@ -194,9 +202,13 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
|
|||||||
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
|
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
|
||||||

|

|
||||||
|
|
||||||
**①类加载检查:** 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
|
#### Step1:类加载检查
|
||||||
|
|
||||||
**②分配内存:** 在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。
|
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
|
||||||
|
|
||||||
|
#### Step2:分配内存
|
||||||
|
|
||||||
|
在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。
|
||||||
|
|
||||||
|
|
||||||
**内存分配的两种方式:(补充内容,需要掌握)**
|
**内存分配的两种方式:(补充内容,需要掌握)**
|
||||||
@ -212,14 +224,17 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
|
|||||||
- **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。**
|
- **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。**
|
||||||
- **TLAB:** 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
|
- **TLAB:** 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
|
||||||
|
|
||||||
|
#### Step3:初始化零值
|
||||||
|
|
||||||
|
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
|
||||||
|
|
||||||
**③初始化零值:** 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
|
#### Step4:设置对象头
|
||||||
|
|
||||||
**④设置对象头:** 初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
|
初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
|
||||||
|
|
||||||
|
#### Step5:执行 init 方法
|
||||||
|
|
||||||
**⑤执行 init 方法:** 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`<init>` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `<init>` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
|
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`<init>` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `<init>` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
|
||||||
|
|
||||||
|
|
||||||
### 3.2 对象的内存布局
|
### 3.2 对象的内存布局
|
||||||
@ -245,13 +260,11 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于**
|
|||||||
**这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。**
|
**这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 四 重点补充内容
|
## 四 重点补充内容
|
||||||
|
|
||||||
### String 类和常量池
|
### 4.1 String 类和常量池
|
||||||
|
|
||||||
**1 String 对象的两种创建方式:**
|
**String 对象的两种创建方式:**
|
||||||
|
|
||||||
```java
|
```java
|
||||||
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
|
String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
|
||||||
@ -272,7 +285,7 @@ System.out.println(str2==str3);//false
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
**2 String 类型的常量池比较特殊。它的主要使用方法有两种:**
|
**String 类型的常量池比较特殊。它的主要使用方法有两种:**
|
||||||
|
|
||||||
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
|
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
|
||||||
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
|
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。
|
||||||
@ -285,7 +298,8 @@ System.out.println(str2==str3);//false
|
|||||||
System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
|
System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,
|
||||||
System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
|
System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象
|
||||||
```
|
```
|
||||||
**3 String 字符串拼接**
|
**字符串拼接:**
|
||||||
|
|
||||||
```java
|
```java
|
||||||
String str1 = "str";
|
String str1 = "str";
|
||||||
String str2 = "ing";
|
String str2 = "ing";
|
||||||
@ -300,7 +314,7 @@ System.out.println(str2==str3);//false
|
|||||||

|

|
||||||
|
|
||||||
尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
|
尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
|
||||||
### String s1 = new String("abc");这句话创建了几个字符串对象?
|
### 4.2 String s1 = new String("abc");这句话创建了几个字符串对象?
|
||||||
|
|
||||||
**将创建 1 或 2 个字符串。如果池中已存在字符串文字“abc”,则池中只会创建一个字符串“s1”。如果池中没有字符串文字“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。**
|
**将创建 1 或 2 个字符串。如果池中已存在字符串文字“abc”,则池中只会创建一个字符串“s1”。如果池中没有字符串文字“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。**
|
||||||
|
|
||||||
@ -320,7 +334,7 @@ false
|
|||||||
true
|
true
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8种基本类型的包装类和常量池
|
### 4.3 8 种基本类型的包装类和常量池
|
||||||
|
|
||||||
- **Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。**
|
- **Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。**
|
||||||
- **两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。**
|
- **两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。**
|
||||||
@ -401,8 +415,3 @@ i4=i5+i6 true
|
|||||||
- <http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/>
|
- <http://www.pointsoftware.ch/en/under-the-hood-runtime-data-areas-javas-memory-model/>
|
||||||
- <https://dzone.com/articles/jvm-permgen-%E2%80%93-where-art-thou>
|
- <https://dzone.com/articles/jvm-permgen-%E2%80%93-where-art-thou>
|
||||||
- <https://stackoverflow.com/questions/9095748/method-area-and-permgen>
|
- <https://stackoverflow.com/questions/9095748/method-area-and-permgen>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user