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

面试题

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

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

分析

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

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

逃逸分析

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

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

判断依据有两个

  1. 对象是否被存入堆中(静态字段或堆中对象的实例字段)

  2. 对象是否被传入未知代码中(方法的调用者和参数)

我们来分析下这两个依据

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

public class Escape {    private static User u;    public static void alloc() {        u = new User(1, "baiya");    }}

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

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

public class Escape {    private static User u;     public static void alloc(User user) {        u = user;    }}

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

栈上分配

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

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

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

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

vs 编译器的堆空间不足_原创|面试官:Java对象一定分配在堆上吗?相关推荐

  1. Java对象都是在堆上分配空间吗?答案竟然是...

    作者 l Hollis 来源 l Hollis(ID:hollischuang) Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或 ...

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

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

  3. 对象可以在栈上分配空间吗?_Java面试题之:Java中所有的对象都分配在堆中吗?...

    JVM中的内存划分暂不讨论,单说堆(Heap),堆中一般存放的是new出来的对象.但是,随着JIT(即时编译)编译器的发展与逃逸分析(Escape Analysis)技术逐渐成熟,栈上分配.标量替换优 ...

  4. 让C++对象只能分配到堆/栈和静态区上并判断分配位置

    对象分配到堆上的过程:三个形式的new 要把对象分配到栈上,需要使用到new operator,而new operator会调用operator new和placement new. operator ...

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

    为了防止歧义,可以换个说法:Java对象实例和数组元素都是在堆上分配内存的吗? 答:不一定.满足特定条件时,它们可以在(虚拟机)栈上分配内存. JVM内存结构很重要,多多复习 这和我们平时的理解可能有 ...

  6. eureka自我保护时间_阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默...

    文章首发:阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默 什么是服务注册? 首先我们来了解下,服务注册.服务发现和服务注册中心的之间的关系. 举个形象的例子,三者之间的关系就好像是供货 ...

  7. 独占设备的分配与回收_灵魂拷问:Java对象的内存分配过程是如何保证线程安全的?...

    点击上方"linkoffer", 选择关注公众号高薪职位第一时间送达 作者 l Hollis JVM内存结构,是很重要的知识,相信每一个静心准备过面试的程序员都可以清楚的把堆.栈. ...

  8. kafka是什么_技术面试官问:Kafka为什么速度那么快?

    Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka的特性之一就是高吞吐率. 即使是普通的服务器,Kafka也可以轻松支持每秒 ...

  9. java中用new创建一个对象的过程解析_【漫画】Java对象的创建和访问全过程详解...

    https://github.com/TangBean 漫画由小猿编写创作 仔细看下面的流程图,我们先来获取一个直观的认识,然后再一点一点的进行详细分析! 对象的创建(遇到一条 new 指令时)检查这 ...

最新文章

  1. java设置窗体关闭时执行某些操作
  2. oracle进城有哪些,oracle主要进程详解
  3. eclispse调试为什么什么都看不到_【科普6】单号出了为什么还是看不到物流信息?...
  4. html app从上向下弹框,移动端从底部向上过渡弹出弹框
  5. vue 学习之路 —— 图片的引入
  6. String使用注意一
  7. 软件开发,维护与支持的困惑
  8. 机器学习算法_机器学习之EM算法和概率图模型
  9. Win11玩永劫无间闪退怎么办?Win11玩永劫无间闪退的解决方法
  10. GLib-CRITICAL : g_variant_get_uint32: assertion ‘g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32)
  11. 如何让网页字体文件大瘦身?前端字体优化知多D
  12. 通达信服务器在哪个文件里,通达信“指标模块”存放在哪个文件夹里
  13. 使用a标签下载文件,解决页面跳转的问题
  14. ConfuserEx
  15. android4.4 计算器,卡西欧仿真计算器
  16. RFID在图书馆系统管理中的有哪些应用优势?
  17. 远程桌面连接命令mstsc怎么用?如何使用mstsc进行远程登录?
  18. 20162327WJH使用队列:模拟票务站台代码分析
  19. DFB激光器电流温度与波长的关系
  20. Mac OS 下的Vim使用系统剪切板

热门文章

  1. C#.NET学习笔记1---C#.NET简介
  2. python数据结构二叉树的前序,中序,后续遍历与推导
  3. SQLite学习手册(目录)
  4. crfpp python
  5. VC中使用GetModuleFileName获取应用程序路径
  6. Java中int和Integer的区别
  7. 计算机二级access上机题,计算机二级ACCESS上机题库
  8. 随想录(动态库的特点)
  9. 分形图案c语言源程序,Mandlbrot集图形的一个C语言实现
  10. linux tcp 阻塞时间,TCP的阻塞和重传机制