diff --git a/README.md b/README.md index 25d5258089c..19267fccd93 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ CAP 也就是 Consistency(一致性)、Availability(可用性)、Partiti **BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 -关于 CAP 的详细解读请看:[《BASE理论解读》](docs/system-design/distributed-system/BASE理论.md)。 +关于 BASE 的详细解读请看:[《BASE理论解读》](docs/system-design/distributed-system/BASE理论.md)。 #### Paxos 算法和 Raft 算法 diff --git "a/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" index 14cc3d83da7..2dc4d6cb192 100644 --- "a/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -136,7 +136,16 @@ Java 语言既具有编译型语言的特征,也具有解释型语言的特征 3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题; 4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能; 5. Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; -6. Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。 +6. Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。 + +🌈 拓展一下: + +- BCL 协议(Oracle Binary Code License Agreement): 可以使用JDK(支持商用),但是不能进行修改。 +- OTN 协议(Oracle Technology Network License Agreement): 11 及之后新发布的JDK用的都是这个协议,可以自己私下用,但是商用需要付费。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/20210425151835918.png) + +相关阅读👍:[《Differences Between Oracle JDK and OpenJDK》](https://www.baeldung.com/oracle-jdk-vs-openjdk) ### Java 和 C++的区别? diff --git "a/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" index 34bafbbed3f..a2e142939bc 100644 --- "a/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" +++ "b/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -1,36 +1,36 @@ - [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-种基本类型的包装类和常量池) - - [参考](#参考) - - [公众号](#公众号) + - [写在前面 (常见面试题)](#写在前面-常见面试题) + - [基本问题](#基本问题) + - [拓展问题](#拓展问题) + - [一 概述](#一-概述) + - [二 运行时数据区域](#二-运行时数据区域) + - [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-种基本类型的包装类和常量池) + - [参考](#参考) + - [公众号](#公众号) @@ -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 程序计数器 + 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。**字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。** 另外,**为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。** @@ -102,7 +103,7 @@ Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成 **Java 虚拟机栈会出现两种错误:`StackOverFlowError` 和 `OutOfMemoryError`。** - **`StackOverFlowError`:** 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。 -- **`OutOfMemoryError`:** Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常异常。 +- **`OutOfMemoryError`:** Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出`OutOfMemoryError`异常异常。 ![](./pictures/java内存区域/《深入理解虚拟机》第三版的第2章-虚拟机栈.png) @@ -131,11 +132,11 @@ Java 方法有两种返回方式: Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。** -**Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。** +Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。 Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** -在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分: +在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常被分为下面三部分: 1. 新生代内存(Young Generation) 2. 老生代(Old Generation) @@ -149,34 +150,32 @@ JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 **上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。** -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 -> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 +> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > > ```c++ > uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { > //survivor_capacity是survivor空间的大小 -> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); -> size_t total = 0; -> uint age = 1; -> while (age < table_size) { -> total += sizes[age];//sizes数组是每个年龄段对象大小 -> if (total > desired_survivor_size) break; -> age++; -> } -> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; +> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); +> size_t total = 0; +> uint age = 1; +> while (age < table_size) { +> total += sizes[age];//sizes数组是每个年龄段对象大小 +> if (total > desired_survivor_size) break; +> age++; +> } +> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } > > ``` -> -> -堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如: +堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如: -1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。 +1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。 2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发`java.lang.OutOfMemoryError: Java heap space` 错误。(和本机物理内存无关,和你配置的内存大小有关!) 3. ...... @@ -188,7 +187,7 @@ JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 #### 2.5.1 方法区和永久代的关系 -> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。 +> 《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 **方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。** 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。 #### 2.5.2 常用参数 @@ -214,12 +213,17 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1 #### 2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢? +下图来自《深入理解 Java 虚拟机》第 3 版 2.2.5 + +![](https://img-blog.csdnimg.cn/20210425134508117.png) + 1. 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。 ->当元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace` + + > 当元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace` -你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。 +你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。 -2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。 +2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。 3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。 @@ -229,18 +233,15 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。 -~~**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。**~~ +~~**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。**~~ -> 修正([issue747](https://github.com/Snailclimb/JavaGuide/issues/747),[reference](https://blog.csdn.net/q5706503/article/details/84640762)): +> 修正([issue747](https://github.com/Snailclimb/JavaGuide/issues/747),[reference](https://blog.csdn.net/q5706503/article/details/84640762)): > -> 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 +> 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 直接内存 @@ -250,23 +251,23 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** 本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 - ## 三 HotSpot 虚拟机对象探秘 + 通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。 ### 3.1 对象的创建 + 下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。 ![Java创建对象的过程](./pictures/java内存区域/Java创建对象的过程.png) #### Step1:类加载检查 - 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 +虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 #### Step2:分配内存 在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 - **内存分配的两种方式:(补充内容,需要掌握)** 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的 @@ -290,8 +291,7 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** #### Step5:执行 init 方法 - 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 - +在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,`` 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 `` 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 ### 3.2 对象的内存布局 @@ -304,22 +304,20 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** **对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 ### 3.3 对象的访问定位 -建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: -1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; +建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**① 使用句柄**和**② 直接指针**两种: + +1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; ![对象的访问定位-使用句柄](./pictures/java内存区域/对象的访问定位-使用句柄.png) -2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 +2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 ![对象的访问定位-直接指针](./pictures/java内存区域/对象的访问定位-直接指针.png) - - **这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。** - -## 四 重点补充内容 +## 四 重点补充内容 ### 4.1 String 类和常量池 @@ -346,33 +344,44 @@ System.out.println(str2==str3);//false **String 类型的常量池比较特殊。它的主要使用方法有两种:** -- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 -- 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。 +1. 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 +2. 如果不是用双引号声明的 String 对象,可以使用 String 提供的 `intern()` 方法。`String.intern()` 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。 + +JDK8 : ```java - String s1 = new String("计算机"); - 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 对象 +String s1 = "计算机"; +String s2 = s1.intern(); +String s3 = "计算机"; +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 的引用 +2. 接下来的 s2 会先去常量池里找,找到对应引用,故指向堆里的 s1 +3. 故 `s1.equals(s2)` 为 true + **字符串拼接:** ```java - String str1 = "str"; - String str2 = "ing"; - - String str3 = "str" + "ing";//常量池中的对象 - String str4 = str1 + str2; //在堆上创建的新的对象 - String str5 = "string";//常量池中的对象 - System.out.println(str3 == str4);//false - System.out.println(str3 == str5);//true - System.out.println(str4 == str5);//false +String str1 = "str"; +String str2 = "ing"; + +String str3 = "str" + "ing";//常量池中的对象 +String str4 = str1 + str2; //在堆上创建的新的对象 +String str5 = "string";//常量池中的对象 +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 个字符串对象。** @@ -395,7 +404,7 @@ true ### 4.3 8 种基本类型的包装类和常量池 -**Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。** 为啥把缓存设置为[-128,127]区间?([参见issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和资源之间的权衡。 +**Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。如果超出对应范围仍然会去创建新的对象。** 为啥把缓存设置为[-128,127]区间?([参见 issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和资源之间的权衡。 ```java public static Boolean valueOf(boolean b) { @@ -404,14 +413,14 @@ public static Boolean valueOf(boolean b) { ``` ```java -private static class CharacterCache { +private static class CharacterCache { private CharacterCache(){} - - static final Character cache[] = new Character[127 + 1]; - static { - for (int i = 0; i < cache.length; i++) - cache[i] = new Character((char)i); - } + + static final Character cache[] = new Character[127 + 1]; + static { + for (int i = 0; i < cache.length; i++) + cache[i] = new Character((char)i); + } } ``` @@ -429,7 +438,7 @@ private static class CharacterCache { System.out.println(i3 == i4);// 输出 false ``` -**Integer 缓存源代码:** +**Integer 缓存源代码:** ```java /** @@ -444,14 +453,16 @@ private static class CharacterCache { ``` **应用场景:** + 1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 -2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 +2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 ```java Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);//输出 false ``` + **Integer 比较更丰富的一个例子:** ```java @@ -461,13 +472,13 @@ private static class CharacterCache { Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); - + System.out.println("i1=i2 " + (i1 == i2)); System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); System.out.println("i1=i4 " + (i1 == i4)); System.out.println("i4=i5 " + (i4 == i5)); - System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); - System.out.println("40=i5+i6 " + (40 == i5 + i6)); + System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); + System.out.println("40=i5+i6 " + (40 == i5 + i6)); ``` 结果: @@ -493,4 +504,4 @@ i4=i5+i6 true - - - -- 深入解析String#intern +- 深入解析 String#intern \ No newline at end of file diff --git "a/docs/java/jvm/\346\234\200\351\207\215\350\246\201\347\232\204JVM\345\217\202\346\225\260\346\214\207\345\215\227.md" "b/docs/java/jvm/\346\234\200\351\207\215\350\246\201\347\232\204JVM\345\217\202\346\225\260\346\214\207\345\215\227.md" index 6dfe0087c3b..319c9d3a7df 100644 --- "a/docs/java/jvm/\346\234\200\351\207\215\350\246\201\347\232\204JVM\345\217\202\346\225\260\346\214\207\345\215\227.md" +++ "b/docs/java/jvm/\346\234\200\351\207\215\350\246\201\347\232\204JVM\345\217\202\346\225\260\346\214\207\345\215\227.md" @@ -126,8 +126,6 @@ JVM具有四种类型的*GC*实现: -Xloggc:/path/to/gc.log ``` - - ## 推荐阅读 - [CMS GC 默认新生代是多大?](https://www.jianshu.com/p/832fc4d4cb53) diff --git "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" "b/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" index 13142bdee6e..f186180a8ec 100644 --- "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" +++ "b/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" @@ -1,14 +1,14 @@ - [类的生命周期](#类的生命周期) - - [类加载过程](#类加载过程) - - [加载](#加载) - - [验证](#验证) - - [准备](#准备) - - [解析](#解析) - - [初始化](#初始化) - - [卸载](#卸载) - - [公众号](#公众号) + - [类加载过程](#类加载过程) + - [加载](#加载) + - [验证](#验证) + - [准备](#准备) + - [解析](#解析) + - [初始化](#初始化) + - [卸载](#卸载) + - [公众号](#公众号) @@ -16,7 +16,6 @@ 一个类的完整生命周期如下: - ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/类加载过程-完善.png) ## 类加载过程 @@ -29,13 +28,13 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 ### 加载 -类加载过程的第一步,主要完成下面3件事情: +类加载过程的第一步,主要完成下面 3 件事情: 1. 通过全类名获取定义此类的二进制字节流 2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 -3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口 +3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口 -虚拟机规范上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。 +虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 `ZIP` 包中读取(日后出现的 `JAR`、`EAR`、`WAR` 格式的基础)、其他文件生成(典型应用就是 `JSP`)等等。 **一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。** @@ -51,16 +50,19 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 **准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: -1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 -2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。 +1. 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 `static` 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 +2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[《深入理解Java虚拟机(第3版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75) +3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。 + +**基本数据类型的零值** : (图片来自《深入理解 Java 虚拟机》第 3 版 7.33 ) + -**基本数据类型的零值:** ![基本数据类型的零值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/基本数据类型的零值.png) ### 解析 -解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。 +解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。 符号引用就是一组符号来描述目标,可以是任何字面量。**直接引用**就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。 @@ -74,49 +76,48 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 对于` ()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 ` ()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。 -对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): +对于初始化阶段,虚拟机严格规范了有且只有 5 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): -1. 当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 - - 当jvm执行new指令时会初始化类。即当程序创建一个类的实例对象。 - - 当jvm执行getstatic指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 - - 当jvm执行putstatic指令时会初始化类。即程序给类的静态变量赋值。 - - 当jvm执行invokestatic指令时会初始化类。即程序调用类的静态方法。 -2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如Class.forname("..."), newInstance()等等。如果类没初始化,需要触发其初始化。 +1. 当遇到 `new` 、 `getstatic`、`putstatic` 或 `invokestatic` 这 4 条直接码指令时,比如 `new` 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 + - 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。 + - 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。 + - 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。 + - 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。 +2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 Class.forname("..."), newInstance()等等。如果类没初始化,需要触发其初始化。 3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。 -5. MethodHandle和VarHandle可以看作是轻量级的反射调用机制,而要想使用这2个调用, - 就必须先使用findStaticVarHandle来初始化要调用的类。 -6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745)」** 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 +5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, + 就必须先使用 findStaticVarHandle 来初始化要调用的类。 +6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745)」** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 ## 卸载 -> 卸载这部分内容来自 [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662)由 **[guang19](https://github.com/guang19)** 补充完善。 +> 卸载这部分内容来自 [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662)由 **[guang19](https://github.com/guang19)** 补充完善。 -卸载类即该类的Class对象被GC。 +卸载类即该类的 Class 对象被 GC。 -卸载类需要满足3个要求: +卸载类需要满足 3 个要求: -1. 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。 +1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。 2. 该类没有在其他任何地方被引用 -3. 该类的类加载器的实例已被GC +3. 该类的类加载器的实例已被 GC -所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。 +所以,在 JVM 生命周期类,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。 -只要想通一点就好了,jdk自带的BootstrapClassLoader, ExtClassLoader, AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 +只要想通一点就好了,jdk 自带的 `BootstrapClassLoader`, `ExtClassLoader`, `AppClassLoader` 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 **参考** -- 《深入理解Java虚拟机》 -- 《实战Java虚拟机》 +- 《深入理解 Java 虚拟机》 +- 《实战 Java 虚拟机》 - ## 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file