作者:LittleMagic

https://www.jianshu.com/p/8377e09971b8

为了防止歧义,可以换个说法:

Java对象实例和数组元素都是在堆上分配内存的吗?

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

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

这和我们平时的理解可能有些不同。虚拟机栈一般是用来存储基本数据类型、引用和返回地址的,怎么可以存储实例数据了呢?

这是因为Java JIT(just-in-time)编译器进行的两项优化,分别称作逃逸分析(escape analysis)和标量替换(scalar replacement)。

注意看一下JIT的位置

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

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

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

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

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

关于逃逸分析,大家可以看下这篇文章:面试问我 Java 逃逸分析,瞬间被秒杀了。以下的例子说明了一种对象逃逸的可能性。

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());
}

推荐去我的博客阅读更多:

1.Java JVM、集合、多线程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架构、阿里巴巴等大厂最新面试题

觉得不错,别忘了点赞+转发哦!

Java中的对象都是在堆上分配的吗?的更多相关文章

  1. 【性能优化】面试官:Java中的对象都是在堆上分配的吗?

    写在前面 从开始学习Java的时候,我们就接触了这样一种观点:Java中的对象是在堆上创建的,对象的引用是放在栈里的,那这个观点就真的是正确的吗?如果是正确的,那么,面试官为啥会问:"Jav ...

  2. python中一切皆是对象,对象都是在堆上存放的,一切都是指针

    1 由于对象都是在堆上存放的,所以,返回值可以任意返回. 这样看来,闭包里面的外部函数的内部变量也是对象,所以,当返回的内部函数被调用时,这个外部函数的变量就没有被释放. 这样看来,返回时,不需要考虑 ...

  3. Java中对象并不是都在堆上分配内存的

    转(https://blog.51cto.com/13906751/2153924) 前段时间,给星球的球友们专门码了一篇文章<深入分析Java的编译原理>,其中深入的介绍了Java中的j ...

  4. 求你了,别再说Java对象都是在堆内存上分配空间的了!

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ...

  5. 别再说Java对象都是在堆内存上分配空间的了!

    Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ...

  6. JVM知识(一) 求你了,别再说Java对象都是在堆内存上分配空间的了!

    求你了,别再说Java对象都是在堆内存上分配空间的了! https://baijiahao.baidu.com/s?id=1661296872935371634&wfr=spider& ...

  7. (转)浅谈Java中的对象和对象引用

    原文地址: http://www.cnblogs.com/dolphin0520/p/3592498.html 在Java中,有一组名词经常一起出现,它们就是"对象和对象引用",很 ...

  8. 浅谈Java中的对象和引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  9. [转]Java中的对象和对象引用实例浅析

    在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起了解一下对象和对象引用之间的区别 ...

随机推荐

  1. 初级知识六——C#事件通知系统实现(观察者模式运用)

    观察者模式,绝对是游戏中十分重要的一种模式,运用这种模式,可以让游戏模块间的通信变得简单,耦合度也会大大降低,下面讲解如何利用C#实现事件通知系统. 补充,首先说下这个系统的实现原理,不然一头扎进去就 ...

  2. 08_Python的数据类型

    1.数字(整型int 浮点型float 复数complex) 1.数字概述 不可变类型,一旦创建不可修改,不是迭代对象,属于原子型 2.整型数int 概述: 整型数是不带有小数部分的数字,包括自然数, ...

  3. 为什么 char 数组比 String 更适合存储密码?

    推荐阅读:5 个刁钻的 String 面试题! 另一个基于 String 的棘手 Java 问题,相信我只有很少的 Java 程序员可以正确回答这个问题. 这是一个真正艰难的核心 Java 面试问题, ...

  4. 剑指 Offer 19. 正则表达式匹配

    题目描述 请实现一个函数用来匹配包含'. '和'*'的正则表达式.模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次).在本题中,匹配是指字符串的所有字符匹配整个模式. ...

  5. 官网安装Python包太慢?教你三种下载安装方式-PiP、conda、轮子,教你三种Pytorch的下载安装方式,保证你再也不用出现Error

    上一期我们介绍了CUDA下载安装以及其总结,这一期教大家如何在Anaconda中使用CUDA来进行加速.神经网络依赖cuDNN的下载安装,以及下载和安装Pytorch-GPU安装包的三种方式(cond ...

  6. 数据库行转列的sql语句

    问题描述 假设有张学生成绩表(CJ)如下Name Subject Result张三 语文 80张三 数学 90张三 物理 85李四 语文 85李四 数学 92李四 物理 82 现在 想写 sql 语句 ...

  7. Linux下 ls 命令的高级用法8例

    Linux下 ls 命令的高级用法8例 在Linux下,ls这个命令大家肯定太熟悉了,良许相信只要是Linux工程师,每天都会离不开这个命令,而且一天会使用个几百次.但是,除了 ls -l 以外,你还 ...

  8. 实践案例丨基于 Raft 协议的分布式数据库系统应用

    摘要:简单介绍Raft协议的原理.以及存储节点(Pinetree)如何应用 Raft实现复制的一些工程实践经验. 1.引言 在华为分布式数据库的工程实践过程中,我们实现了一个计算存储分离. 底层存储基 ...

  9. python3中抛异常except后面参数

    try: xxx except exception as e: print("给exception取了个别名叫做e") else: ccc

  10. Spring系列之初识Spring Spring概述

    初始Spring 啥是Spring? 下面这个就是Spring Spring当然不是上面那个Spring,Spring之所以命名为Spring是因为这个开源的轻量级的开源框架的出现给软件行业带来了春天 ...