Skip to content

Commit e521f53

Browse files
committed
Java
1 parent f0860d7 commit e521f53

615 files changed

Lines changed: 1712 additions & 50741 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.DS_Store

0 Bytes
Binary file not shown.

docs/.DS_Store

2 KB
Binary file not shown.

docs/JVM/.DS_Store

6 KB
Binary file not shown.

docs/JVM/1_JVM.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
## 一、运行时数据区域
2+
3+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/5778d113-8e13-4c53-b5bf-801e58080b97.png" width="400px"> </div>
4+
5+
### 程序计数器(Program Counter Register)
6+
7+
- 当前线程所执行的字节码行号指示器(逻辑)
8+
- 通过改变计数器的值来选取下一条需要执行的字节码指令
9+
- 和线程一对一的关系,即“线程私有”
10+
- 对 Java 方法计数,如果是 Native 方法则计数器值为 Undefined
11+
- 只是计数,不会发生内存泄漏
12+
13+
### Java 虚拟机栈
14+
15+
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
16+
17+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/8442519f-0b4d-48f4-8229-56f984363c69.png" width="400px"> </div>
18+
19+
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
20+
21+
```java
22+
java -Xss512M HackTheJava
23+
```
24+
25+
该区域可能抛出以下异常:
26+
27+
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
28+
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
29+
30+
> 局部变量表和操作数栈
31+
32+
- 局部变量表:包含方法执行过程中的所有变量
33+
- 操作数栈:入栈、出栈、复制、交换、产生消费变量
34+
35+
```java
36+
public class JVMTest {
37+
public static int add(int a ,int b) {
38+
int c = 0;
39+
c = a + b;
40+
return c;
41+
}
42+
}
43+
```
44+
45+
```html
46+
javap -verbose JVMTest
47+
```
48+
49+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_1.png" width="400px"> </div>
50+
51+
解读上述指令:
52+
53+
- stack = 2 说明栈的深度是 2 ;locals = 3 说明有 3 个本地变量 ;args_size = 2 说明该方法需传入 2 个参
54+
- load 指令表示入操作数栈,store 表示出操作数栈
55+
56+
执行 add(1,2),说明局部变量表和操作数栈的关系:
57+
58+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_2.png" > </div>
59+
60+
### 本地方法栈
61+
62+
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
63+
64+
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
65+
66+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/66a6899d-c6b0-4a47-8569-9d08f0baf86c.png" width="300px"> </div>
67+
68+
###
69+
70+
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
71+
72+
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
73+
74+
- 新生代(Young Generation)
75+
- 老年代(Old Generation)
76+
77+
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
78+
79+
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
80+
81+
```java
82+
java -Xms1M -Xmx2M HackTheJava
83+
```
84+
85+
> Java 内存分配策略
86+
87+
1、静态存储:编译时确定每个数据目标在运行时的存储空间需求
88+
89+
2、栈式存储:数据区需求在编译时未知,运行时模块入口前确定
90+
91+
3、堆式存储:编译时或运行时模块入口都无法确定,动态分配
92+
93+
> JVM 内存模型中堆和栈的联系
94+
95+
引用对象、数组时,栈里定义变量保存堆中目标的首地址。
96+
97+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_3.png" > </div>
98+
99+
> JVM 内存模型中堆和栈的区别
100+
101+
管理方式:栈自动释放,堆需要 GC
102+
103+
空间大小:栈比堆小
104+
105+
碎片相关:栈产生的碎片远小于堆
106+
107+
分配方式:栈支持静态和动态分配,而堆仅支持动态分配
108+
109+
效率:栈的效率比堆高
110+
111+
### 方法区
112+
113+
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
114+
115+
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
116+
117+
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
118+
119+
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
120+
121+
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。**元空间存储类的元信息,静态变量和常量池等放入堆中**
122+
123+
> 元空间(MetaSpace)与永久代(PermGen)的区别
124+
125+
元空间使用本地内存,而永久代使用 JVM 的内存。
126+
127+
> 元空间(MetaSpace)相比永久代(PermGen)的优势
128+
129+
1、字符串常量池存在永久代中,容易出现性能问题和内存溢出
130+
131+
2、类和方法的信息大小难以确定,给永久代的大小指定带来困难
132+
133+
3、永久代会为 GC 带来不必要的复杂性
134+
135+
### 运行时常量池
136+
137+
运行时常量池是方法区的一部分。
138+
139+
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
140+
141+
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
142+
143+
### 直接内存
144+
145+
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
146+
147+
### JVM常见参数
148+
149+
| 参数 | 含义 |
150+
| ---- | ------------------------------------------------------------ |
151+
| -Xss | 规定了每个线程虚拟机栈(堆栈)的大小,会影响并发线程数的大小 |
152+
| -Xms | 堆的初始值 |
153+
| -Xmx | 堆能达到的最大值 |

docs/JVM/2_JVM.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
## 二、HotSpot 虚拟机对象
2+
3+
### 对象的创建
4+
5+
对象的创建步骤:
6+
7+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_4.jpg" > </div>
8+
9+
1. **类加载检查**
10+
11+
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的**符号引用**
12+
并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。
13+
如果没有,那必须先执行相应的类加载过程。
14+
15+
2. **分配内存**
16+
17+
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
18+
对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,
19+
而Java堆是否规整又由所采用的**垃圾收集器是否带有压缩整理功能**决定。
20+
21+
- 内存分配的两种方式
22+
23+
| 内存分配的两种方式 | **指针碰撞** | **空闲列表** |
24+
| :----------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
25+
| 适用场景 | 堆内存规整(即没有内存碎片)的情况 | 堆内存不规整的情况 |
26+
| 原理 | 用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将指针移动一段与对象大小相等的距离 | 虚拟机会维护一个列表,在该列表和总分记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块划分给对象示例,然后更新列表记录 |
27+
| GC收集器 | Serial ParNew | CMS |
28+
29+
- 内存分配并发问题
30+
31+
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,
32+
作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
33+
34+
(1)CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
35+
36+
(2)TLAB: 每一个线程预先在Java堆中分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才采用上述的CAS进行内存分配。
37+
38+
3. **初始化零值**
39+
40+
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作**保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用**,程序能访问到这些字段的数据类型所对应的零值。
41+
42+
4. **设置对象头**
43+
44+
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。
45+
这些信息存放在对象头中。
46+
另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
47+
48+
5. **执行init方法**
49+
50+
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,
51+
但从 Java 程序的视角来看,对象创建才刚开始,**\<init\> 方法还没有执行,所有的字段都还为零**
52+
所以一般来说,执行 new 指令之后会接着执行 \<init \> 方法,
53+
**对象按照程序员的意愿进行初始化**,这样一个真正可用的对象才算完全产生出来。
54+
55+
### 对象的内存布局
56+
57+
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据、对齐填充。
58+
59+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_5.png" > </div>
60+
61+
62+
63+
- **对象头**
64+
65+
Hotspot虚拟机的对象头包括两部分信息:
66+
67+
一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等等),
68+
69+
另一部分是类型指针,即对象指向它的**类元数据的指针**,虚拟机通过这个指针来**确定这个对象是哪个类的实例**
70+
71+
- **实例数据**
72+
73+
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
74+
75+
- **对齐填充**
76+
77+
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起**占位**作用。
78+
因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,
79+
换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),
80+
因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
81+
82+
### 对象的访问定位
83+
84+
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。
85+
对象的访问方式视虚拟机的实现而定,目前主流的访问方式有两种:使用句柄、直接指针。
86+
87+
- **使用句柄**
88+
89+
如果使用[句柄](https://zh.wikipedia.org/wiki/%E5%8F%A5%E6%9F%84)的话,那么 **Java 堆**中将会划分出一块内存来作为句柄池,reference 中存储的就是**对象的句柄地址**,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 。
90+
91+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_6.png" > </div>
92+
93+
- **直接指针**
94+
95+
如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是**对象的地址**
96+
97+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_7.png" > </div>
98+
99+
这两种对象访问方式各有优势:
100+
101+
1、使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而**reference本身不需要修改**
102+
103+
2、使用直接指针访问方式最大的好处就是**速度快**,它节省了一次指针定位的时间开销。

docs/JVM/3_JVM.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
## 三、String 类和常量池
2+
3+
### 1、String 对象的两种创建方式
4+
5+
```java
6+
String str1 = "abcd";
7+
String str2 = new String("abcd");
8+
System.out.println(str1==str2); //false
9+
```
10+
11+
这两种不同的创建方法是有差别的:
12+
13+
第一种方式是在常量池中获取对象("abcd" 属于字符串字面量,因此编译时期会在常量池中创建一个字符串对象);
14+
15+
第二种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abcd" 字符串对象)。
16+
17+
- "abcd" 属于字符串字面量,因此编译时期会在常量池中创建一个字符串对象,该字符串对象指向这个 "abcd" 字符串字面量;
18+
- 使用 new 的方式会在堆中创建一个字符串对象。
19+
20+
str1 指向常量池中的 “abcd”,而 str2 指向堆中的字符串对象。
21+
22+
### 2、intern() 方法
23+
24+
intern() 方法设计的初衷,就是重用 String 对象,以节省内存消耗。
25+
26+
JDK6:当调用intern方法的时候,如果字符串常量池先前已创建出该字符串对象,则返回常量池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
27+
28+
JDK6+:当调用intern方法的时候,如果字符串常量池先前已创建出该字符串对象,则返回常量池中的该字符串的引用。**否则,如果该字符串对象已存在与Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用**;如果堆中不存在,则在常量池中创建该字符串并返回其引用。
29+
30+
在 JVM 运行时数据区中的方法区有一个常量池,但是发现在 JDK 1.6 以后常量池被放置在了堆空间,因此常量池位置的不同影响到了 String 的 intern() 方法的表现。
31+
32+
```java
33+
String s = new String("1");
34+
s.intern();
35+
String s2 = "1";
36+
System.out.println(s == s2);
37+
38+
String s3 = new String("1") + new String("1");
39+
s3.intern();
40+
String s4 = "11";
41+
System.out.println(s3 == s4);
42+
```
43+
44+
> JDK 1.6 及以下
45+
46+
- 上述代码输出结果:
47+
48+
```html
49+
false
50+
false
51+
```
52+
53+
- 解释:
54+
55+
在 JDK 1.6 中所有的输出结果都是 false,因为 JDK 1.6 以及以前版本中,常量池是放在 PermGen 区(属于方法区)中的,而方法区和堆区是完全分开的。
56+
57+
**使用引号声明的字符串会直接在字符串常量池中生成**的,而 new 出来的 String 对象是放在堆空间中的。所以两者的内存地址肯定是不相同的,即使调用了 intern() 方法也是不影响的。
58+
59+
intern() 方法在 JDK 1.6 中的作用:比如 `String s = new String("1");`,再调用 `s.intern()`,此时返回值还是字符串"1",表面上看起来好像这个方法没什么用处。但实际上,在 JDK1.6 中:**检查字符串常量池里是否存在 "1" 这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把 "1" 添加到字符串常量池中,然后再返回它的引用**
60+
61+
> JDK 1.6 及以上
62+
63+
- 上述代码输出结果:
64+
65+
```html
66+
false
67+
true
68+
```
69+
70+
- 解释:
71+
72+
`String s= new String("1")` 生成了字符串常量池中的 "1" 和堆空间中的字符串对象。
73+
74+
`s.intern()` s 对象去字符串常量池中寻找后,发现 "1" 已存在于常量池中。
75+
76+
`String s2 = "1"` 生成 s2 的引用指向常量池中的 "1" 对象。
77+
78+
显然,s 和 s2 的引用地址是不同的。
79+
80+
`String s3 = new String("1") + new String("1") `在字符串常量池中生成 "1",并在堆空间中生成 s3 引用指向的对象(内容为 "11")。 *注意此时常量池中是没有 "11" 对象*
81+
82+
`s3.intern()`将 s3 中的 "11" 字符串放入字符串常量池中。 JDK 1.6 的做法是直接在常量池中生成一个 "11" 的对象。但**在 JDK 1.7 中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用**。这份引用直接指向 s3 引用的对象,也就是说 `s3.intern() == s3 `会返回 true。
83+
84+
`String s4 = "11"`, 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时 s4 就是指向 s3 引用对象的一个引用。因此 `s3 == s4 `返回了true。
85+
86+
### 3、字符串拼接
87+
88+
```java
89+
String str1 = "str";
90+
String str2 = "ing";
91+
92+
String str3 = "str" + "ing";//常量池中的对象
93+
String str4 = str1 + str2; //TODO:在堆上创建的新的对象
94+
String str5 = "string";//常量池中的对象
95+
System.out.println(str3 == str4);//false
96+
System.out.println(str3 == str5);//true
97+
System.out.println(str4 == str5);//false
98+
```
99+
100+
<div align="center"> <img src="https://gitee.com/duhouan/ImagePro/raw/master/JVM/j_8.jpg" width="400px"> </div>
101+
102+
注意:尽量避免多个字符串拼接,因为这样会重新创建对象。 如果需要改变字符串的话,可以使用 **StringBuilder** 或者 **StringBuffer**
103+
104+
> 面试题:String s1 = new String("abc");问创建了几个对象?
105+
106+
创建2个字符串对象(前提是 String Pool 中还没有 "abcd" 字符串对象)。
107+
108+
- "abc" 属于字符串字面量,因此编译时期会**在常量池中创建一个字符串对象**,指向这个 "abcd" 字符串字面量;
109+
- 使用 new 的方式会在堆中创建一个字符串对象。
110+
111+
(字符串常量"abc"在**编译期**就已经确定放入常量池,而 Java **堆上的"abc"是在运行期**初始化阶段才确定)。
112+
113+
```
114+
String s1 = new String("abc");// 堆内存的地址值
115+
String s2 = "abc";
116+
System.out.println(s1 == s2);// 输出false
117+
//因为一个是堆内存,一个是常量池的内存,故两者是不同的。
118+
System.out.println(s1.equals(s2));// 输出true
119+
```

0 commit comments

Comments
 (0)