我做了一些实验,以便了解Hotspot何时可以进行堆栈分配.事实证明,它的堆栈分配比基于available documentation的预期要有限得多.Choi“Escape Analysis for Java”引用的文章表明,只分配给局部变量的对象总是可以堆栈分配.但事实并非如此.

所有这些都是当前Hotspot实现的实现细节,因此它们可能会在将来的版本中进行更改.这是指我的OpenJDK安装,它是X86-64的版本1.8.0_121.

基于相当多的实验,简短的总结似乎是:

如果热点可以堆栈分配对象实例

>所有用途都是内联的

>永远不会将其分配给任何静态或对象字段,仅分配给局部变量

>在程序的每个点,哪些局部变量包含对象的引用必须是JIT时间可确定的,并且不依赖于任何不可预测的条件控制流.

>如果对象是数组,则其大小必须在JIT时知道,并且索引必须使用JIT时间常量.

要知道这些条件何时适用,您需要了解Hotspot的工作原理.由于涉及许多非本地因素,依赖于Hotspot在某种情况下确定堆栈分配可能是有风险的.特别是知道是否所有内容都很难预测.

实际上,如果你只是使用它们进行迭代,那么简单的迭代器通常可以是栈可分配的.对于复合对象,只能对外层对象进行堆栈分配,因此列表和其他集合总是会导致堆分配.

如果你有一个HashMap< Integer,Something>并且你在myHashMap.get(42)中使用它,42可以在测试程序中堆栈分配,但它不会在完整的应用程序中,因为你可以确定在HashMaps中将有两种以上的密钥对象整个程序,因此键上的hashCode和equals方法不会内联.

除此之外,我没有看到任何普遍适用的规则,它将取决于代码的细节.

热点内部

第一个重要的事情是在内联后执行转义分析.这意味着Hotspot的转义分析在这方面比Choi论文中的描述更强大,因为从方法返回但在调用方法本地的对象仍然可以进行堆栈分配.因此,如果您执行此操作,则迭代器几乎总是可以进行堆栈分配. for(Foo item:myList){…}(myList.iterator()的实现很简单,它们通常都是.)

Hotspot只有在确定方法“热”时才编译优化版本的方法,因此很多次运行的代码根本没有得到优化,在这种情况下,没有堆栈分配或内联.但对于那些你通常不在乎的方法.

内联

内联决策基于Hotspot首先收集的分析数据.声明的类型并不重要,即使方法是虚拟的,Hotspot也可以根据它在分析期间看到的对象的类型来内联它.类似的东西适用于分支(即if语句和其他控制流构造):如果在分析期间Hotspot从未看到某个分支被采用,它将基于从不采用分支的假设来编译和优化代码.在这两种情况下,如果Hotspot无法证明其假设始终为真,则会在已编译的代码中插入检查,称为“不常见的陷阱”,如果遇到此类陷阱,Hotspot将进行去优化并可能重新优化新信息考虑在内.

Hotspot将分析哪些对象类型作为呼叫站点的接收者.如果Hotspot只看到一个类型或在调用站点只发现两种不同的类型,则它能够内联调用的方法.如果只有一个或两个非常常见的类型,而其他类型的出现频率低得多,Hotspot还应该能够内联常见类型的方法,包括检查它需要采取哪些代码. (我不完全确定最后一种情况,有一两种常见类型和更多不常见的类型).如果有两种以上的常见类型,Hotspot根本不会内联调用,而是生成间接调用的机器代码.

这里的“类型”是指对象的确切类型.不考虑已实现的接口或共享超类.即使在调用站点出现不同的接收器类型,但它们都继承了方法的相同实现(例如,所有从Object继承hashCode的多个类),Hotspot仍将生成间接调用而不是内联调用. (所以i.m.o.在这种情况下,热点是非常愚蠢的.我希望未来版本能改进这一点.)

Hotspot也只会内联不太大的方法. “不太大”由-XX确定:MaxInlineSize = n和-XX:FreqInlineSize = n选项. JVM字节码大小低于MaxInlineSize的Inlinable方法总是内联的,如果调用是“热”,则内联JVM字节码大小低于FreqInlineSize的方法.更大的方法永远不会内联.默认情况下,MaxInlineSize是35并且FreqInlineSize是平台相关的,但对我来说它是325.所以如果你想让它们内联,请确保你的方法不是太大.它有时可以帮助从大方法中分离出公共路径,以便可以将其内联到其调用者中.

剖析

关于性能分析的一个重要事项是,性能分析站点基于JVM字节码,它本身不以任何方式内联.所以如果你有例如静态方法

static List map(List list, Function func) {

List result = new ArrayList();

for(T item : list) { result.add(func.call(item)); }

return result;

}

映射可以在列表上调用的SAM函数并返回转换后的列表,Hotspot会将对func.call的调用视为单个程序范围的调用站点.您可以在程序中的多个位置调用此地图功能,在每个呼叫站点传递不同的功能(但对于一个呼叫站点则相同).在这种情况下,您可能希望Hotspot能够内联映射,然后调用func.call,因为在每次使用map时,只有一个func类型.如果是这样的话,Hotspot将能够非常紧密地优化循环.不幸的是,Hotspot对此并不够聪明.它只为func.call调用站点保留一个配置文件,将所有传递给它的func类型集中在一起.您可能会使用两个以上不同的func实现,因此Hotspot将无法内联对func.call的调用. Link有更多细节,而archived link原来似乎已经不见了.

(另外,在Kotlin中,等效循环可以完全内联,因为Kotlin编译器可以在字节码级别进行内联调用.因此,对于某些用途,它可能比Java快得多.)

标量替换

另一个重要的事情是Hotspot实际上并没有实现对象的堆栈分配.相反,它实现了标量替换,这意味着对象被解构为其组成字段,并且这些字段是像普通局部变量一样分配的.这意味着根本没有任何物体.标量替换仅在从不需要创建指向堆栈分配对象的指针时才有效.某些形式的堆栈分配在例如C或Go可以在堆栈上分配完整的对象,然后将引用或指针传递给它们到被调用的函数,但在Hotspot中这不起作用.因此,如果需要将对象引用传递给非内联方法,即使引用不会转义被调用的方法,Hotspot也将始终堆分配这样的对象.

原则上,Hotspot可能更聪明,但现在却不是.

测试程序

我使用以下程序和变体来查看Hotspot何时进行标量替换.

// Minimal example for which the JVM does not scalarize the allocation. If field is final, or the second allocation is unconditional, it will.

class Scalarization {

int field = 0xbd;

long foo(long i) { return i * field; }

public static void main(String[] args) {

long result = 0;

for(long i=0; i<100; i++) {

result += test();

}

System.out.println("Result: "+result);

}

static long test() {

long ctr = 0x5;

for(long i=0; i<0x10000; i++) {

Scalarization s = new Scalarization();

ctr = s.foo(ctr);

if(i == 0) s = new Scalarization();

ctr = s.foo(ctr);

}

return ctr;

}

}

如果您使用javac Scalarization.java编译并运行此程序; java -verbose:gc Scalarization你可以看到标量替换是否由垃圾收集的数量起作用.如果标量替换工作,我的系统上没有发生垃圾收集,如果标量替换不起作用,我会看到一些垃圾收集.

Hotspot能够进行scalarize的变种运行速度明显快于不能运行的变种.我验证了生成的机器代码(instructions),以确保Hotspot没有进行任何意外的优化.如果热点能够标量替换分配,那么它还可以在循环上进行一些额外的优化,展开几次迭代,然后将这些迭代组合在一起.因此,在scalarized版本中,每个迭代器执行多个源代码级迭代的工作时,有效循环计数较低.因此速度差异不仅仅是由于分配和垃圾收集开销.

意见

我尝试了上述程序的一些变体.标量替换的一个条件是对象绝不能分配给对象(或静态)字段,并且可能也不会分配给数组.所以在代码中

Foo f = new Foo();

bar.field = f;

Foo对象不能被标量替换.即使条本身被标量替换,并且如果你再也不使用bar.field,这就成立了.因此,只能将对象分配给局部变量.

仅凭这一点还不够,Hotspot还必须能够在JIT时间静态地确定哪个对象实例将成为呼叫的目标.例如,使用以下foo实现以及test和remove字段会导致堆分配:

long foo(long i) { return i * 0xbb; }

static long test() {

long ctr = 0x5;

for(long i=0; i<0x10000; i++) {

Scalarization s = new Scalarization();

ctr = s.foo(ctr);

if(i == 50) s = new Scalarization();

ctr = s.foo(ctr);

}

return ctr;

}

如果然后删除第二个赋值的条件,则不再发生堆分配:

static long test() {

long ctr = 0x5;

for(long i=0; i<0x10000; i++) {

Scalarization s = new Scalarization();

ctr = s.foo(ctr);

s = new Scalarization();

ctr = s.foo(ctr);

}

return ctr;

}

在这种情况下,Hotspot可以静态地确定哪个实例是每次调用s.foo的目标.

另一方面,即使s的第二个赋值是Scalarization的子类,具有完全不同的实现,只要赋值是无条件的,Hotspot仍然会对分配进行scalarize.

Hotspot似乎无法将对象移动到之前被标量替换的堆中(至少在没有去优化的情况下).标量替换是一种全有或全无的事情.因此在原始测试方法中,Scalarization的两个分配总是发生在堆上.

条件语句

一个重要的细节是Hotspot将根据其分析数据预测条件.如果从未执行条件赋值,Hotspot将根据该假设编译代码,然后可能能够进行标量替换.如果在稍后的时间点确实采取了条件,Hotspot将需要使用这个新假设重新编译代码.新代码不会进行标量替换,因为Hotspot无法再静态地确定以下调用的接收器实例.

例如,在这个测试变体中:

static long limit = 0;

static long test() {

long ctr = 0x5;

long i = limit;

limit += 0x10000;

for(; i

Scalarization s = new Scalarization();

ctr = s.foo(ctr);

if(i == 0xf9a0) s = new Scalarization();

ctr = s.foo(ctr);

}

return ctr;

}

条件赋权仅在程序的生命周期内执行一次.如果此分配发生得足够早,在Hotspot开始对测试方法进行完整分析之前,Hotspot从不会注意到所采用的条件并编译执行标量替换的代码.如果在采取条件时已经开始进行分析,则Hotspot将不会进行标量替换.使用0xf9a0的测试值,标量替换是否发生在我的计算机上是不确定的,因为完全在分析开始时可能会有所不同(例如,因为分析和优化的代码是在后台线程上编译的).因此,如果我运行上述变体,它有时会执行一些垃圾收集,有时则不会.

Hotspot的静态代码分析比C/C++和其他静态编译器可以做的更加有限,因此Hotspot在通过几个条件和其他控制结构来跟踪方法中的控制流以确定变量引用的实例时并不聪明即使它对程序员或更智能的编译器是静态可确定的.在许多情况下,分析信息将弥补这一点,但需要注意的是.

数组

如果在JIT时间知道它们的大小,则可以分配堆栈.但是,除非Hotspot还能在JIT时间静态地确定索引值,否则不支持索引到数组中.所以堆栈分配的数组是没用的.由于大多数程序不直接使用数组而是使用标准集合,因此这不是非常相关,因为嵌入对象(例如包含ArrayList中的数据的数组)由于其嵌入式而需要进行堆分配.我认为这种限制的原因是对局部变量不存在索引操作,因此这需要额外的代码生成功能来处理非常罕见的用例.

java让对象分配在栈上_java – Hotspot何时可以在堆栈上分配对象?相关推荐

  1. java锁方法和锁代码块_java的同步方法和同步代码块,对象锁,类锁区别

    /** * @author admin * @date 2018/1/12 9:48 * 做用在同一个实例对象上讨论 * synchronized同步方法的测试 * 两个线程,一个线程调用synchr ...

  2. java 数据库连接不上_JAVA基础(六) 处理连接不上MYSQL数据库的方法

    一 使用环境 假设自己这台机子的IP = 192.168.10.10,局域网内另一台IP=192.168.10.20; 使用MYSQL连接本地数据库的时候,可以使用配置:localhost/127.0 ...

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

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

  4. java创建对象时分配内存方式,是堆上分配还是栈上分配?

    创建对象的内存是分配在堆上还是栈上面?大部分童鞋的回答是这样的:"肯定分配在堆内存的嘛,栈内存是属于子线程和基本数据类型专用的内存空间,怎么会分配到栈上面呢?",这个回答嘛,也对, ...

  5. [转载]如何限制一个类对象只在栈(堆)上分配空间?

    一般情况下,编写一个类,是可以在栈或者堆分配空间.但有些时候,你想编写一个只能在栈或者只能在堆上面分配空间的类.这能不能实现呢?仔细想想,其实也是可以滴. 在C++中,类的对象建立分为两种,一种是静态 ...

  6. JVM - 结合代码示例彻底搞懂Java内存区域_对象在堆-栈-方法区(元空间)之间的关系

    文章目录 Pre 示例demo 总体关系 代码示例论证 反汇编 Pre JVM - 结合代码示例彻底搞懂Java内存区域_线程栈 | 本地方法栈 | 程序计数器 中我们探讨了线程栈中的内部结构 ,大家 ...

  7. 46栈内存溢出、内存区域(程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区、直接内存、内存溢出)与内存溢出(对象实例化分析)

    46.什么情况下会发生栈内存溢出 46.1.Java 内存区域与内存溢出 46.1.1.内存区域 46.1.1.1.程序计数器 46.1.1.2.Java 虚拟机栈 46.1.1.3.本地方法栈 46 ...

  8. java 常量池 和 堆 的关系_Java堆、栈和常量池以及相关String的详细讲解(经典中的经典)...

    博客分类: Java综合 一:在JAVA中,有六个不同的地方可以存储数据: 1. 寄存器(register). 这是最快的存储区,因为它位于不同于其他存储区的地方--处理器内部.但是寄存器的数量极其有 ...

  9. 记录java对象修改过的字段_Java垃圾回收器与内存回收策略

    Java中,内存由虚拟机管理,控制着回收什么,什么时候回收,怎么回收. 在栈中内存的随线程产生和分配,销毁而回收,在堆中,需要制定一系列策略来判断该回收哪些区域,以及何时回收. 可达性分析 主流的做法 ...

最新文章

  1. 【Android 性能优化】应用启动优化 ( 方法追踪代码模板 | 示例项目 | SD 卡访问权限 | 示例代码 | 获取 Trace 文件 | Android Studio 查看文件)
  2. [MSSQL]ROW_NUMBER函数
  3. python画同心圆程序_python – matplotlib:如何在给定的半径范围内绘制同心圆
  4. mysql 中文 问号 utf8_[MySql] 设置了UTF8,中文存数据库中仍然出现问号
  5. POJ3080 ZOJ2784 UVALive3628 Blue Jeans题解
  6. 《程序员的数学》读后感
  7. J1939CANTP
  8. IEEE Access的模板的问题
  9. 电路基础知识 -- 虚短和虚断
  10. 【正点原子MP157连载】第四章 ATK-STM32MP157功能测试-摘自【正点原子】STM32MP157快速体验
  11. python中的pymysql_Mysql在python中的使用:pymysql
  12. 【POJ 2719 --- Faulty Odometer】
  13. 逍遥模拟器微信提示无法连接服务器,轻松解决夜神逍遥模拟器模拟器等模拟器无法连接问题...
  14. ol3 加载天地图服务
  15. 去中心化存储项目终极指南 | Filecoin, Storj 和 PPIO 项目异同(上)
  16. 微课--Python使用UDP协议实现局域网内屏幕广播(40分钟)
  17. Mutex与Semaphore 第二部分 互斥锁
  18. 百度编辑器ueditor添加视频方法
  19. 《 在深渊里仰望星空》读后感
  20. kmalloc使用不当导致内存分配失败问题

热门文章

  1. [Cake] 0.C#Make自动化构建-简介
  2. 微软企业应用开发三大方向:跨平台、开放/开源与DevOps
  3. Windows 10中国定制版完工!更专业
  4. java调c++代码_Java中调用C++代码的实现 | 学步园
  5. [转]史上最全的后端技术大全,你都了解哪些技术呢?
  6. 深夜爆肝:万字长文3种语言实现Huffman树(强烈建议三连)
  7. ArcGIS实验教程——实验三十八:基于ArcGIS的等高线、山体阴影、山顶点提取案例教程
  8. Android之解决NestedScrollView嵌套ViewPager导致出现左右页面滑动冲突
  9. C和指针之函数之归以字符形式按顺序打印数字的每位数字(递归和非递归)
  10. Android之TypedArray 为什么需要调用recycle()