Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Latest commit

 

History

History
History
166 lines (104 loc) · 8.97 KB

File metadata and controls

166 lines (104 loc) · 8.97 KB
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
### JVM内存分配策略
关于JVM的内存结构及内存分配方式不是本文的重点这里只做简单回顾以下是我们知道的一些常识
1根据Java虚拟机规范Java虚拟机所管理的内存包括方法区虚拟机栈本地方法栈程序计数器等
2我们通常认为JVM中运行时数据存储包括堆和栈这里所提到的栈其实指的是虚拟机栈或者说是虚拟栈中的局部变量表
3栈中存放一些基本类型的变量数据int/short/long/byte/float/double/Boolean/char和对象引用
4堆中主要存放对象即通过new关键字创建的对象
5数组引用变量是存放在栈内存中数组元素是存放在堆内存中
深入理解Java虚拟机中关于Java堆内存有这样一段描述
但是随着JIT编译期的发展与逃逸分析技术逐渐成熟栈上分配标量替换优化技术将会导致一些微妙的变化所有的对象都分配到堆上也渐渐变得不那么绝对
这里只是简单提了一句并没有深入分析很多人看到这里由于对JIT逃逸分析等技术不了解所以也无法真正理解上面这段话的含义
**PS这里默认大家都了解什么是JIT不了解的朋友可以先自行Google了解下或者加入我的知识星球阅读那篇球友专享文章。**
其实在编译期间JIT会对代码做很多优化其中有一部分优化的目的就是减少内存堆分配压力其中一种重要的技术叫做**逃逸分析**。
### 逃逸分析
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法通过逃逸分析Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
逃逸分析的基本行为就是分析对象动态作用域当一个对象在方法中被定义后它可能被外部方法所引用例如作为调用参数传递到其他地方中称为方法逃逸
例如
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
StringBuffer sb是一个方法内部变量上述代码中直接将sb返回这样这个StringBuffer有可能被其他方法所改变这样它的作用域就不只是在方法内部虽然它是一个局部变量称其逃逸到了方法外部甚至还有可能被外部线程访问到譬如赋值给类变量或可以在其他线程中访问的实例变量称为线程逃逸
上述代码如果想要StringBuffer sb不逃出方法可以这样写
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
不直接返回 StringBuffer那么StringBuffer将不会逃逸出方法
使用逃逸分析编译器可以对代码做如下优化
同步省略如果一个对象被发现只能从一个线程被访问到那么对于这个对象的操作可以不考虑同步
将堆分配转化为栈分配如果一个对象在子程序中被分配要使指向该对象的指针永远不会逃逸对象可能是栈分配的候选而不是堆分配
分离对象或标量替换有的对象可能不需要作为一个连续的内存结构存在也可以被访问到那么对象的部分或全部可以不存储在内存而是存储在CPU寄存器中
上面的关于同步省略的内容我在《[深入理解多线程)—— Java虚拟机的锁优化技术][1]》中有介绍过即锁优化中的锁消除技术依赖的也是逃逸分析技术
本文主要来介绍逃逸分析的第二个用途将堆分配转化为栈分配
> 其实以上三种优化中栈上内存分配其实是依靠标量替换来实现的由于不是本文重点这里就不展开介绍了如果大家感兴趣我后面专门出一篇文章全面介绍下逃逸分析
在Java代码运行时通过JVM参数可指定是否开启逃逸分析, `-XX:+DoEscapeAnalysis` : 表示开启逃逸分析 `-XX:-DoEscapeAnalysis` : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析如需关闭需要指定`-XX:-DoEscapeAnalysis`
### 对象的栈上内存分配
我们知道在一般情况下对象和数组元素的内存分配是在堆内存上进行的但是随着JIT编译器的日渐成熟很多优化使这种分配策略并不绝对JIT编译器就可以在编译期间根据逃逸分析的结果来决定是否可以将对象的内存分配从堆转化为栈
我们来看以下代码
public static void main(String[] args) {
long a1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
alloc();
}
// 查看执行时间
long a2 = System.currentTimeMillis();
System.out.println("cost " + (a2 - a1) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(100000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
}
static class User {
}
其实代码内容很简单就是使用for循环在代码中创建100万个User对象
**我们在alloc方法中定义了User对象但是并没有在方法外部引用他也就是说这个对象并不会逃逸到alloc外部经过JIT的逃逸分析之后就可以对其内存分配进行优化。**
我们指定以下JVM参数并运行
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
在程序打印出 `cost XX ms` 代码运行结束之前我们使用`[jmap][1]`命令来查看下当前堆内存中有多少个User对象
➜ ~ jps
2809 StackAllocTest
2810 Jps
➜ ~ jmap -histo 2809
num #instances #bytes class name
----------------------------------------------
1: 524 87282184 [I
2: 1000000 16000000 StackAllocTest$User
3: 6806 2093136 [B
4: 8006 1320872 [C
5: 4188 100512 java.lang.String
6: 581 66304 java.lang.Class
从上面的jmap执行结果中我们可以看到堆中共创建了100万个`StackAllocTest$User`实例
在关闭逃避分析的情况下(-XX:-DoEscapeAnalysis),虽然在alloc方法中创建的User对象并没有逃逸到方法外部但是还是被分配在堆内存中也就说如果没有JIT编译器优化没有逃逸分析技术正常情况下就应该是这样的即所有对象都分配到堆内存中
接下来我们开启逃逸分析再来执行下以上代码
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
在程序打印出 `cost XX ms` 代码运行结束之前我们使用`jmap`命令来查看下当前堆内存中有多少个User对象
➜ ~ jps
709
2858 Launcher
2859 StackAllocTest
2860 Jps
➜ ~ jmap -histo 2859
num #instances #bytes class name
----------------------------------------------
1: 524 101944280 [I
2: 6806 2093136 [B
3: 83619 1337904 StackAllocTest$User
4: 8006 1320872 [C
5: 4188 100512 java.lang.String
6: 581 66304 java.lang.Class
从以上打印结果中可以发现开启了逃逸分析之后(-XX:+DoEscapeAnalysis),在堆内存中只有8万多个`StackAllocTest$User`对象也就是说在经过JIT优化之后堆内存中分配的对象数量从100万降到了8万
> 除了以上通过jmap验证对象个数的方法以外读者还可以尝试将堆内存调小然后执行以上代码根据GC的次数来分析也能发现开启了逃逸分析之后在运行期间GC次数会明显减少正是因为很多堆上分配被优化成了栈上分配所以GC次数有了明显的减少
### 总结
所以如果以后再有人问你是不是所有的对象和数组都会在堆内存分配空间
那么你可以告诉他不一定随着JIT编译器的发展在编译期间如果JIT经过逃逸分析发现有些对象没有逃逸出方法那么有可能堆内存分配会被优化成栈内存分配但是这也并不是绝对的就像我们前面看到的一样在开启逃逸分析之后也并不是所有User对象都没有在堆上分配
[1]: http://www.hollischuang.com/archives/2344
Morty Proxy This is a proxified and sanitized view of the page, visit original site.