查看原文
其他

Java中的对象都是在堆上分配的吗?

点击上方 "程序员小乐"关注, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约


每日英文

Everyone's got a life that no one else knows about.

每个人都有自己的人生,冷暖自知。


每日掏心

若要自由,就得牺牲安全。若要闲散,就不能获得别人评价中的成就。若要愉悦,就无须计较身边的人给予的态度。




来自:LittleMagic | 责编:乐乐

链接:jianshu.com/p/8377e09971b8

程序员小乐(ID:study_tech)第 787 次推文   图片来自Pexels


往日回顾:流行的Spring Boot + Vue架构整合开发的网易云+QQ音乐(附源码)!



     

   正文   


为了防止歧义,可以换个说法:
Java对象实例和数组元素都是在堆上分配内存的吗?
答:不一定。满足特定条件时,它们可以在(虚拟机)栈上分配内存。

JVM内存结构很重要,多多复习
这和我们平时的理解可能有些不同。虚拟机栈一般是用来存储基本数据类型、引用和返回地址的,怎么可以存储实例数据了呢?
这是因为Java JIT(just-in-time)编译器进行的两项优化,分别称作逃逸分析(escape analysis)和标量替换(scalar replacement)。

注意看一下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());
}


欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入订阅号程序员小乐技术群,在后台回复“加群”或者“学习”即可。

猜你还想看


阿里、腾讯、百度、华为、京东最新面试题汇集

Java 多态的实现机制,看了都说好!

中央政治局会议:疫情防控工作取得阶段性成效,但拐点尚未到来

在Spring Boot项目中整合使用Activiti

关注订阅号「程序员小乐」,收看更多精彩内容
嘿,你在看吗?
: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存