首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >原创|面试官:Java对象一定分配在堆上吗?

原创|面试官:Java对象一定分配在堆上吗?

作者头像
每天晒白牙
发布于 2020-08-21 07:31:00
发布于 2020-08-21 07:31:00
1.5K00
代码可运行
举报
文章被收录于专栏:每天晒白牙每天晒白牙
运行总次数:0
代码可运行

最近在看 Java 虚拟机方面的资料,以备工作中的不时之需。首先我先抛出一个我自己想的面试题,然后再引出后面要介绍的知识点如逃逸分析、标量替换、栈上分配等知识点

面试题

Java 对象一定分配在堆上吗?

自己先思考下,再往下阅读效果更佳哦!

分析

我们都知道 Java 对象一般分配在堆上,而堆空间又是所有线程共享的。了解 NIO 库的朋友应该知道还有一种是堆外内存也叫直接内存。直接内存是直接向操作系统申请的内存区域,访问直接内存的速度一般会优于堆内存。直接内存的大小不直接受 Xmx 设定的值限制,但是在使用的时候也要注意,毕竟系统内存有限,堆内存和直接内存的总和依然还是会受操作系统的内存限制的。

通过上面的分析,大家也知道了,Java 对象除了可以分配在堆上,还可以直接分配在堆外内存中。但这点不是我今天想讨论的,我想和大家聊聊栈上分配,说到栈上分配就不得不先说下逃逸分析

逃逸分析

逃逸分析是是一种动态确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。

换句话说,逃逸分析的目的是判断对象的作用域是否有可能逃出方法体

判断依据有两个

  1. 对象是否被存入堆中(静态字段或堆中对象的实例字段)
  2. 对象是否被传入未知代码中(方法的调用者和参数)

我们来分析下这两个依据

对于第一点对象是否被存入堆中,我们知道堆内存是线程共享的,一旦对象被分配在堆中,那所有线程都可以访问到该对象,这样即时编译器就追踪不到所有使用到该对象的地方了,这样的对象就属于逃逸对象,如下所示

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Escape {
    private static User u;
    public static void alloc() {
        u = new User(1, "baiya");
    }
}

User 对象属于类 Escape 的成员变量,该对象是可能被所有线程访问的,所以会发生逃逸

第二点是对象是否被传入未知代码中,Java 的即时编译器是以方法为单位进行编译,即时编译器会把方法中未被内联的方法当成未知代码,所以无法判断这个未知方法的方法调用会不会将调用者或参数放到堆中,所以认为方法的调用者和参数是逃逸的,如下所示

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Escape {
    private static User u; 
    public static void alloc(User user) {
        u = user;
    }
}

方法 alloc 的参数 user 被赋值给类 Escape 的成员变量 u,所以也会被所有线程访问,也是会发生逃逸的。

栈上分配

栈上分配是 Java 虚拟机提供的一种优化技术,该技术的基本思想是可以将线程私有的对象打散,分配到栈上,而非堆上。那分配到栈上有什么好处呢? 我们知道栈中的变量会在方法调用结束后自动销毁,所以省掉了 jvm 进行垃圾回收,进而可以提高系统的性能

栈上分配是要基于逃逸分析标量替换实现的

我们通过一个具体的例子来验证下非逃逸分析的对象确实是分配到了栈上

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class OnStack {
    public static void alloc() {
        User user = new User(1, "baiya");
    }
    public static void main(String[] args) {
        long start = Instant.now().toEpochMilli();
        for (int i = 0; i < 100_000_000; i++) {
            alloc();
        }
        long end = Instant.now().toEpochMilli();
        System.out.println("耗时:" + (end - start));
    }
}

上面的代码是循环 1 亿次执行 alloc 方法创建 User 对象,每个 User 对象占用约 16 bytes(怎么计算的下面会说) 空间,创建 1 亿次,所以如果 User 都是在堆上分配的话则需要 1.5G 的内存空间。如果我们设置堆空间小于这个数,应该会发生 gc,如果设置的特别小,应该会发生大量的 gc。

我们用下面的参数执行上述代码

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations 其中 -server 是开启 server 模式,逃逸分析需要 server 模式的支持 -Xmx10 -Xms10m,设置堆内存是 10m,远小于 1.5G -XX:+DoEscapeAnalysis 开启逃逸分析 -XX:+PrintGCDetails 如果发生 gc,打印 gc 日志 -XX:+EliminateAllocations 开启标量替换,允许把对象打散分配在栈上,比如 User 对象,它有两个属性 id 和 name,可以把他们看成独立的局部变量分别进行分配

配置好 jvm 参数后,执行代码,查看结果可知执行了 3 次 gc,耗时 10 毫秒,可以推断出 User 对象并未全部分配到堆上,而是把绝大多数分配到了堆上,分配在堆上的好处是方法结束后自动释放对应的内存,是一种优化手段。

栈上分配

我们上面说了栈上分配依赖逃逸分析和标量替换,那么我们可以破坏其中任意一个条件,去掉逃逸分析就可以通过 -XX:-DoEscapteAnalysis 或者关闭标量替换 -XX:-EliminateAllocations 再去执行上述代码,观察执行情况,发现发生了大量的 gc,并且耗时 3182 毫秒,执行时间远远高于上面的 10 毫秒,所以可以推测出并未执行栈上分配的优化手段

堆上分配

计算 User 对象占用空间大小

对象由四部分构成

  1. 对象头:记录一个对象的实例名字、ID和实例状态。 普通对象占用 8 bytes,数组占用 12 bytes (8 bytes 的普通对象头 + 4 bytes 的数组长度)
  2. 基本类型 boolean,byte 占用 1 byte char,short 占用 2 bytes int,float 占用 4 bytes long,double 占用 8 bytes
  3. 引用类型:每个引用类型占用 4 bytes
  4. 填充物:以 8 的倍数计算,不足 8 的倍数会自动补齐

我们上面的 User 对象有两个属性,一个 int 类型的 id 占用 4 bytes,一个引用类型的 name 占用 4bytes,在加上 8 bytes 的对象头,正好是 16 bytes

总结

关于虚拟机的知识点还有很多而且也比较重要,如果懂对写优质代码、优化性能、排查问题等都是锦上添花,比如逃逸分析,即时编译器会根据逃逸分析的结果进行优化,如锁消除以及标量替换。感兴趣的朋友可以自己查查资料学习下。通过这个栈上分配的例子,以后我们写代码时,把可以不逃逸的对象写进方法体中,这样就会被编译器优化,提升性能。而且也知道了上面面试题的答案,就是 Java 中的对象并不一定分配在堆上,也可能分配在栈上

参考资料
  1. 《实战Java虚拟机》
  2. 《深入理解Java虚拟机》
  3. https://zh.wikipedia.org/wiki/%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天晒白牙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JVM 对象分配过程
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。
斯武丶风晴
2020/05/09
1.1K0
JVM 对象分配过程
对象并不一定都是在堆上分配内存的
关于JVM的内存结构及内存分配方式,不是本文的重点,这里只做简单回顾。以下是我们知道的一些常识:
爱撸猫的杰
2019/03/28
7190
jvm之逃逸分析解读
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。 
一个风轻云淡
2023/10/15
2390
8.JVM内存分配机制超详细解析
之前研究过类的加载过程。具体详情可查看文章:https://www.cnblogs.com/ITPower/p/15356099.html
用户7798898
2021/10/15
1.6K0
java栈内存初始化,阿里面试官:小伙子,你给我说一下JVM对象创建与内存分配机制吧…
虚拟机遇到一条new指令(new关键字、对象的克隆、对象的序列化等)时,会先去检查这个指令的参数在常量池中定位到一个类的符号引用,并且这个符号引用代表的类是否应被加载过,如果没有那么就去加载该类
全栈程序员站长
2022/08/28
3380
java栈内存初始化,阿里面试官:小伙子,你给我说一下JVM对象创建与内存分配机制吧…
深入分析JVM逃逸分析对性能的影响
逃逸分析(Escape Analysis) 逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。 方法逃逸的几种方式如下: public class EscapeTest { public static Object obj; public void globalVariableEscape() { // 给全局变量赋值,发生逃逸
java404
2018/05/18
1.4K0
JVM简介—1.Java内存区域
Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域,这些区域各有各的用途以及各自的创建和销毁时间也不一样。有的区域会随着虚拟机的进程启动而存在,有的区域则依赖用户线程的启动和结束而进而跟着建立和销毁。
东阳马生架构
2025/03/10
690
JVM堆
堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。
麋鹿大哥
2020/08/19
4070
Java 对象都是在堆上分配内存吗?
来源:LittleMagic jianshu.com/p/8377e09971b8
用户1516716
2019/12/10
1.1K0
Java 对象都是在堆上分配内存吗?
Java的对象一定是在堆上分配的嘛?谁这么说就直接用“逃逸分析”反驳他!
之前在和朋友聊天的时候,他突然问我什么是“逃逸分析”。说实话当时我还真不太能完整的讲出什么是逃逸分析。这玩意虽然我看八股的时候经常遇见,但之前还真没专项学习过。因此我们今天来完整的介绍一下什么是逃逸分析。
程序员牛肉
2025/01/22
970
Java的对象一定是在堆上分配的嘛?谁这么说就直接用“逃逸分析”反驳他!
JVM之堆
一个进程对应一个jvm实例,同时包含多个线程,这些线==程共享方法区和堆==,每个==线程独有程序计数器、本地方法栈和虚拟机栈==。
程序员阿杜
2021/06/29
9250
面试官:Java中实例对象存储在哪?
低级语言是计算机认识的语言、高级语言是程序员认识的语言。如何从高级语言转换成低级语言呢?这个过程其实就是编译。
Java宝典
2021/03/16
6690
虚拟机--逃逸分析
如果对象发生逃逸,那会分配到堆中。(因为对象发生了逃逸,就代表这个对象可以被外部访问,换句话说,就是可以共享,能共享数据的,无非就是堆或方法区,这里就是堆。)
终码一生
2022/04/14
4430
再清楚不过了,JVM逃逸分析,你一定得知道
提到JVM,相信大家一定知道JVM是什么?但是,提到逃逸分析,相信大多数人都可能一脸懵逼,逃逸分析到底是什么呢?接下来给大家分享一下。
Java程序猿阿谷
2020/12/16
2.4K0
再清楚不过了,JVM逃逸分析,你一定得知道
JVM之堆
约定:新生区 <–> 新生代 <–> 年轻代 、 养老区 <–> 老年区 <–> 老年代、 永久区 <–> 永久代
Java微观世界
2025/01/20
1530
JVM之堆
JVM本地方法栈&堆
上一节我们介绍了程序计数器和Java虚拟机栈,今天我们一起了解一下关于本地方法栈和Java堆的相关知识。
shysh95
2020/07/16
6360
JVM本地方法栈&堆
JVM-彻底搞懂 逃逸分析&标量替换
通过上图的对象分配流程,我们可以知道逃逸分析是发生在第一步判断对象是否可以在栈上分配的时候, 在栈上分配的目的是为了减少将对象分配到堆上的概率,节约堆内存,减少GC压力。
小小工匠
2021/08/17
2.1K0
5. java 对象是如何创建的?new背后到底做了什么
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
源码之路
2021/03/02
8610
5. java 对象是如何创建的?new背后到底做了什么
【性能优化】面试官:Java中的对象和数组都是在堆上分配的吗?
https://github.com/sunshinelyz/mykit-delay
冰河
2020/10/29
2.3K0
【性能优化】面试官:Java中的对象和数组都是在堆上分配的吗?
Java对象一定分配在堆上吗?
Java中创建的对象一般会分配到堆上,当堆空间不足时,就会触发GC进行垃圾回收,但是GC次数太多会影响程序的性能。
阿珍
2024/11/04
2300
Java对象一定分配在堆上吗?
相关推荐
JVM 对象分配过程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档