Java 对象都是在堆上分配内存吗?


点击上方 "IT牧场"
,选择 "设为星标"

技术干货每日送达!

来源:LittleMagic jianshu.com/p/8377e09971b8

为了防止歧义,可以换个说法:Java对象实例和数组元素都是在堆上分配内存的吗?

答:不一定。满足特定条件时,它们可以在(虚拟机)栈上分配内存。

Java 对象都是在堆上分配内存吗?

JVM内存结构很重要,多多复习

这和我们平时的理解可能有些不同。虚拟机栈一般是用来存储基本数据类型、引用和返回地址的,怎么可以存储实例数据了呢?这是因为Java JIT(just-in-time)编译器进行的两项优化,分别称作 逃逸分析(escape analysis)
标量替换(scalar replacement)
。JIT是个复杂的话题,本文不赘述,看官如果想进一步了解的话,可以参考这篇文章,它里面提供了几篇有用的参考资料。

Java 对象都是在堆上分配内存吗?

注意看一下JIT的位置

中文维基上对逃逸分析的描述基本准确,摘录如下:

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。

如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中被访问到的地方无法确定——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,因为全局变量是可以在当前子程序之外访问的,此时指针也发生了逃逸。

逃逸分析确定某个指针可以存储的所有地方,以及确定能否保证指针的生命周期只在当前进程或线程中。

简单来讲,JVM中的逃逸分析可以通过分析对象引用的使用范围(即动态作用域),来决定对象是否要在堆上分配内存,也可以做一些其他方面的优化。

以下的例子说明了一种对象逃逸的可能性。

  static StringBuilder getStringBuilder1(String a, String b) {
StringBuilder builder = new StringBuilder(a);
builder.append(b);
return builder; // builder通过方法返回值逃逸到外部
}

static String getStringBuilder2(String a, String b) {
StringBuilder builder = new StringBuilder(a);
builder.append(b);
return builder.toString(); // builder范围维持在方法内部,未逃逸
}

以JDK 1.8为例,可以通过设置JVM参数 -XX:+DoEscapeAnalysis
-XX:-DoEscapeAnalysis
来开启或关闭逃逸分析(默认当然是开启的)。下面先写一个没有对象逃逸的例子。

public class EscapeAnalysisTest {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 5000000; i++) {
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
Thread.sleep(600000);
}

static void allocate() {
MyObject myObject = new MyObject(2019, 2019.0);
}

static class MyObject {
int a;
double b;

MyObject(int a, double b) {
this.a = a;
this.b = b;
}
}
}

然后通过开启和关闭 DoEscapeAnalysis
开关观察不同。

  • 关闭逃逸分析

~ java -XX:-DoEscapeAnalysis EscapeAnalysisTest
76 ms
~ jmap -histo 26031
num #instances #bytes class name
----------------------------------------------
1: 5000000 120000000 me.lmagics.EscapeAnalysisTest$MyObject
2: 636 12026792 [I
3: 3097 1524856 [B
4: 5088 759960 [C
5: 3067 73608 java.lang.String
6: 623 71016 java.lang.Class
7: 727 43248 [Ljava.lang.Object;
8: 532 17024 java.io.File
9: 225 14400 java.net.URL
10: 334 13360 java.lang.ref.Finalizer
# ......
  • 开启逃逸分析

~ java -XX:+DoEscapeAnalysis EscapeAnalysisTest
4 ms
~ jmap -histo 26655
num #instances #bytes class name
----------------------------------------------
1: 592 11273384 [I
2: 90871 2180904 me.lmagics.EscapeAnalysisTest$MyObject
3: 3097 1524856 [B
4: 5088 759952 [C
5: 3067 73608 java.lang.String
6: 623 71016 java.lang.Class
7: 727 43248 [Ljava.lang.Object;
8: 532 17024 java.io.File
9: 225 14400 java.net.URL
10: 334 13360 java.lang.ref.Finalizer
# ......

可见,关闭逃逸分析之后,堆上有5000000个MyObject实例,而开启逃逸分析之后,就只剩下90871个实例了,不管是实例数还是内存占用都只有原来的2%不到。另外,如果把堆内存限制得小一点(比如加上 -Xms10m -Xmx10m
),并且打印GC日志( -XX:+PrintGCDetails
)的话,关闭逃逸分析还会造成频繁的GC,开启逃逸分析就没有这种情况。这说明逃逸分析确实降低了堆内存的压力

但是,逃逸分析只是栈上内存分配的前提,接下来还需要进行标量替换才能真正实现。

所谓标量,就是指JVM中无法再细分的数据,比如int、long、reference等。相对地,能够再细分的数据叫做聚合量。仍然考虑上面的例子,MyObject就是一个聚合量,因为它由两个标量a、b组成。通过逃逸分析,JVM会发现myObject没有逃逸出allocate()方法的作用域,标量替换过程就会将myObject直接拆解成a和b,也就是变成了:

  static void allocate() {
int a = 2019;
double b = 2019.0;
}

可见,对象的分配完全被消灭了,而int、double都是基本数据类型,直接在栈上分配就可以了。所以, 在对象不逃逸出作用域并且能够分解为纯标量表示时
,对象就可以在栈上分配。

JVM提供了参数 -XX:+EliminateAllocations
来开启标量替换,默认仍然是开启的。显然,如果把它关掉的话,就相当于禁止了栈上内存分配,只有逃逸分析是无法发挥作用的。在Debug版JVM中,还可以通过参数 -XX:+PrintEliminateAllocations
来查看标量替换的具体情况。

除了标量替换之外,通过逃逸分析还能实现 同步消除(synchronization elision)
,当然它与本文的主题无关了。举个例子:

  private void someMethod() {
Object lockObject = new Object();
synchronized (lockObject) {
System.out.println(lockObject.hashCode());
}
}

lockObject这个对象的生命期只在someMethod()方法中,并不存在多线程访问的问题,所以synchronized块并无意义,会被优化掉:

  private void someMethod() {
Object lockObject = new Object();
System.out.println(lockObject.hashCode());
}

累了,晚安晚安。

干货分享

最近将个人学习笔记整理成册,使用PDF分享。关注我,回复如下代码,即可获得百度盘地址,无套路领取!



001:《Java并发高并发解决方案》学习笔记;



002:《深入JVM内核——原理、诊断与优化》学习笔记;



003:《Java面试宝典》



004:《Docker开源书》



005:《Kubernetes开源书》



006:《DDD速成(领域驱动设计速成)》



007: 全部



008: 加技术讨论群

近期热文

想知道更多?长按/扫码关注我吧↓↓↓
Java 对象都是在堆上分配内存吗?
>>>技术讨论群<<<

喜欢就点个 "在看"
呗^_^

原文 

http://mp.weixin.qq.com/s?__biz=MzI4ODQ3NjE2OA==&mid=2247485939&idx=1&sn=4c304ece70cca199b9b043ea97de709d

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » Java 对象都是在堆上分配内存吗?

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址