diff --git a/docs/java/可能是把Java内存区域讲的最清楚的一篇文章.md b/docs/java/可能是把Java内存区域讲的最清楚的一篇文章.md index 68c4bd5b..754d3b98 100644 --- a/docs/java/可能是把Java内存区域讲的最清楚的一篇文章.md +++ b/docs/java/可能是把Java内存区域讲的最清楚的一篇文章.md @@ -1,30 +1,41 @@ - -- [写在前面(常见面试题)](#写在前面常见面试题) - - [基本问题](#基本问题) - - [拓展问题](#拓展问题) -- [一 概述](#一-概述) -- [二 运行时数据区域](#二-运行时数据区域) - - [2.1 程序计数器](#21-程序计数器) - - [2.2 Java 虚拟机栈](#22-java-虚拟机栈) - - [2.3 本地方法栈](#23-本地方法栈) - - [2.4 堆](#24-堆) - - [2.5 方法区](#25-方法区) - - [2.6 运行时常量池](#26-运行时常量池) - - [2.7 直接内存](#27-直接内存) -- [三 HotSpot 虚拟机对象探秘](#三-hotspot-虚拟机对象探秘) - - [3.1 对象的创建](#31-对象的创建) - - [3.2 对象的内存布局](#32-对象的内存布局) - - [3.3 对象的访问定位](#33-对象的访问定位) -- [四 重点补充内容](#四--重点补充内容) - - [String 类和常量池](#string-类和常量池) - - [String s1 = new String("abc");这句话创建了几个对象?](#string-s1--new-stringabc这句话创建了几个对象) - - [8种基本类型的包装类和常量池](#8种基本类型的包装类和常量池) -- [参考](#参考) +- [Java 内存区域详解](#java-内存区域详解) + - [写在前面 (常见面试题)](#写在前面-常见面试题) + - [基本问题](#基本问题) + - [拓展问题](#拓展问题) + - [一 概述](#一-概述) + - [二 运行时数据区域](#二-运行时数据区域) + - [2.1 程序计数器](#21-程序计数器) + - [2.2 Java 虚拟机栈](#22-java-虚拟机栈) + - [2.3 本地方法栈](#23-本地方法栈) + - [2.4 堆](#24-堆) + - [2.5 方法区](#25-方法区) + - [2.5.1 方法区和永久代的关系](#251-方法区和永久代的关系) + - [2.5.2 常用参数](#252-常用参数) + - [2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?](#253-为什么要将永久代-permgen-替换为元空间-metaspace-呢) + - [2.6 运行时常量池](#26-运行时常量池) + - [2.7 直接内存](#27-直接内存) + - [三 HotSpot 虚拟机对象探秘](#三-hotspot-虚拟机对象探秘) + - [3.1 对象的创建](#31-对象的创建) + - [Step1:类加载检查](#step1类加载检查) + - [Step2:分配内存](#step2分配内存) + - [Step3:初始化零值](#step3初始化零值) + - [Step4:设置对象头](#step4设置对象头) + - [Step5:执行 init 方法](#step5执行-init-方法) + - [3.2 对象的内存布局](#32-对象的内存布局) + - [3.3 对象的访问定位](#33-对象的访问定位) + - [四 重点补充内容](#四--重点补充内容) + - [4.1 String 类和常量池](#41-string-类和常量池) + - [4.2 String s1 = new String("abc");这句话创建了几个字符串对象?](#42-string-s1--new-stringabc这句话创建了几个字符串对象) + - [4.3 8 种基本类型的包装类和常量池](#43-8-种基本类型的包装类和常量池) + - [参考](#参考) -## 写在前面(常见面试题) + +# Java 内存区域详解 + +## 写在前面 (常见面试题) ### 基本问题 @@ -34,28 +45,26 @@ ### 拓展问题 -- **String类和常量池** -- **8种基本类型的包装类和常量池** - +- **String 类和常量池** +- **8 种基本类型的包装类和常量池** ## 一 概述 -对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。 - +对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++程序开发程序员这样为内一个 new 操作去写对应的 delete/free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。 ## 二 运行时数据区域 Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。 -**JDK 1.8之前:** +**JDK 1.8 之前:**
- +
**JDK 1.8 :**
- +
**线程私有的:** @@ -68,8 +77,7 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成 - 堆 - 方法区 -- 直接内存(非运行时数据区的一部分) - +- 直接内存 (非运行时数据区的一部分) ### 2.1 程序计数器 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。** @@ -85,24 +93,24 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成 ### 2.2 Java 虚拟机栈 -**与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。** +**与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。** -**Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** (实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。) +**Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。** (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。) -**局部变量表主要存放了编译器可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 +**局部变量表主要存放了编译器可知的各种数据类型**(boolean、byte、char、short、int、float、long、double)、**对象引用**(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 **Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。** -- **StackOverFlowError:** 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。 -- **OutOfMemoryError:** 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 +- **StackOverFlowError:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。 +- **OutOfMemoryError:** 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。 -Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 +Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。 **扩展:那么方法/函数如何调用?** -Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。 +Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。 -Java方法有两种返回方式: +Java 方法有两种返回方式: 1. return 语句。 2. 抛出异常。 @@ -120,49 +128,49 @@ Java方法有两种返回方式: ### 2.4 堆 Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** -Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** +Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。**
- +
-上图所示的 eden区、s0区、s1区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 +上图所示的 eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 ### 2.5 方法区 -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。 +方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。 方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。 -#### 方法区和永久代的关系 +#### 2.5.1 方法区和永久代的关系 -> 《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久带这一说法。 +> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久带这一说法。 -#### 常用参数 +#### 2.5.2 常用参数 JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小 ```java --XX:PermSize=N //方法区(永久代)初始大小 --XX:MaxPermSize=N //方法区(永久代)最大大小,超过这个值将会抛出OutOfMemoryError异常:java.lang.OutOfMemoryError: PermGen +-XX:PermSize=N //方法区 (永久代) 初始大小 +-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen ``` 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。** -JDK 1.8 的时候,方法区(HotSpot的永久代)被彻底移除了(JDK1.7就已经开始了),取而代之是元空间,元空间使用的是直接内存。 +JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。 下面是一些常用参数: ```java --XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小) --XX:MaxMetaspaceSize=N //设置Metaspace的最大大小 +-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小) +-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小 ``` 与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。 -#### 为什么要将永久代(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 将根据运行时的应用程序需求动态地重新调整大小。 当然这只是其中一个原因,还有很多底层的原因,这里就不提了。 @@ -172,7 +180,7 @@ JDK 1.8 的时候,方法区(HotSpot的永久代)被彻底移除了(JDK1. 既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。 -**JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。** +**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。** ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-14/26038433.jpg) ——图片来源:https://blog.csdn.net/wangbiao007/article/details/78545189 @@ -192,11 +200,15 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** ### 3.1 对象的创建 下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。 -![Java对象的创建过程](https://user-gold-cdn.xitu.io/2018/8/22/16561e59a4135869?w=950&h=279&f=png&s=28529) +![Java 对象的创建过程 ](https://user-gold-cdn.xitu.io/2018/8/22/16561e59a4135869?w=950&h=279&f=png&s=28529) -**①类加载检查:** 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 +#### Step1:类加载检查 -**②分配内存:** 在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 + 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 + +#### Step2:分配内存 + +在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 **内存分配的两种方式:(补充内容,需要掌握)** @@ -210,51 +222,52 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** 在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全: - **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 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 + 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 ### 3.2 对象的内存布局 -在 Hotspot 虚拟机中,对象在内存中的布局可以分为3块区域:**对象头**、**实例数据**和**对齐填充**。 +在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:**对象头**、**实例数据**和**对齐填充**。 -**Hotspot虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈希码、GC分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 +**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 **实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 -**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 +**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 ### 3.3 对象的访问定位 -建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: +建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: -1. **句柄:** 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; -![使用句柄](https://user-gold-cdn.xitu.io/2018/4/27/16306b9573968946?w=786&h=362&f=png&s=109201) +1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; +![使用句柄 ](https://user-gold-cdn.xitu.io/2018/4/27/16306b9573968946?w=786&h=362&f=png&s=109201) -2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。 +2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 -![使用直接指针](https://user-gold-cdn.xitu.io/2018/4/27/16306ba3a41b6b65?w=766&h=353&f=png&s=99172) +![使用直接指针 ](https://user-gold-cdn.xitu.io/2018/4/27/16306ba3a41b6b65?w=766&h=353&f=png&s=99172) **这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。** - - ## 四 重点补充内容 -### String 类和常量池 +### 4.1 String 类和常量池 -**1 String 对象的两种创建方式:** +**String 对象的两种创建方式:** ```java -String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后str1指向字符串常量池中的对象,如果有,则直接将str1指向"abcd""; +String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd""; String str2 = new String("abcd");//堆中创建一个新的对象 String str3 = new String("abcd");//堆中创建一个新的对象 System.out.println(str1==str2);//false @@ -266,13 +279,13 @@ System.out.println(str2==str3);//false - 第一种方式是在常量池中拿对象; - 第二种方式是直接在堆内存空间创建一个新的对象。 -记住一点:**只要使用new方法,便需要创建新的对象。** +记住一点:**只要使用 new 方法,便需要创建新的对象。** 再给大家一个图应该更容易理解,图片来源:: ![String-Pool-Java](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3String-Pool-Java1-450x249.png) -**2 String 类型的常量池比较特殊。它的主要使用方法有两种:** +**String 类型的常量池比较特殊。它的主要使用方法有两种:** - 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 - 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。 @@ -282,10 +295,11 @@ System.out.println(str2==str3);//false String s2 = s1.intern(); String s3 = "计算机"; System.out.println(s2);//计算机 - System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象, - System.out.println(s3 == s2);//true,因为两个都是常量池中的String对象 + System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象, + System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象 ``` -**3 String 字符串拼接** +**字符串拼接:** + ```java String str1 = "str"; String str2 = "ing"; @@ -300,17 +314,17 @@ System.out.println(str2==str3);//false ![](https://user-gold-cdn.xitu.io/2018/8/22/16561e59a4d13f92?w=593&h=603&f=png&s=22265) 尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 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 个字符串对象。** **验证:** ```java String s1 = new String("abc");// 堆内存的地址值 String s2 = "abc"; - System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。 - System.out.println(s1.equals(s2));// 输出true + System.out.println(s1 == s2);// 输出 false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。 + System.out.println(s1.equals(s2));// 输出 true ``` **结果:** @@ -320,28 +334,28 @@ false 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 并没有实现常量池技术。** ```java Integer i1 = 33; Integer i2 = 33; - System.out.println(i1 == i2);// 输出true + System.out.println(i1 == i2);// 输出 true Integer i11 = 333; Integer i22 = 333; - System.out.println(i11 == i22);// 输出false + System.out.println(i11 == i22);// 输出 false Double i3 = 1.2; Double i4 = 1.2; - System.out.println(i3 == i4);// 输出false + System.out.println(i3 == i4);// 输出 false ``` **Integer 缓存源代码:** ```java /** -*此方法将始终缓存-128到127(包括端点)范围内的值,并可以缓存此范围之外的其他值。 +*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) @@ -352,15 +366,15 @@ true ``` **应用场景:** -1. Integer i1=40;Java 在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 +1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 ```java Integer i1 = 40; Integer i2 = new Integer(40); - System.out.println(i1==i2);//输出false + System.out.println(i1==i2);//输出 false ``` -**Integer比较更丰富的一个例子:** +**Integer 比较更丰富的一个例子:** ```java Integer i1 = 40; @@ -391,18 +405,13 @@ i4=i5+i6 true 解释: -语句i4 == i5 + i6,因为+这个操作符不适用于Integer对象,首先i5和i6进行自动拆箱操作,进行数值相加,即i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。 +语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。 ## 参考 -- 《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版》 -- 《实战java虚拟机》 +- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 +- 《实战 java 虚拟机》 - - - - - - - - -