热点何时可以在堆栈上分配对象? - java

由于Java 6周围的某个地方,热点JVM可以进行转义分析并在堆栈上而不是在垃圾收集堆上分配非转义对象。这样可以加快生成代码的速度,并减少垃圾收集器上的压力。

Hotspot能够堆叠分配对象的规则是什么?换句话说,何时可以依靠它进行堆栈分配?

编辑:这个问题是重复的,但是(IMO)与原始问题相比,以下答案是一个更好的答案。

参考方案

我做了一些实验,以查看Hotspot何时可以堆栈分配。事实证明,它的堆栈分配比基于available documentation的预期要有限得多。 Choi的参考文章“ Java的Escape Analysis”建议,只有始终分配给局部变量的对象才能始终进行堆栈分配。但这不是事实。

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

基于大量实验的简短摘要似乎是:

如果出现以下情况,热点可以堆栈分配对象实例

其所有用途均内联
它永远不会分配给任何静态或对象字段,而只会分配给局部变量
在程序的每个点上,哪些局部变量包含对对象的引用,都必须在JIT时确定,并且不依赖于任何不可预测的条件控制流。
如果对象是数组,则必须在JIT时间知道其大小,并且索引必须使用JIT时间常数。

要知道这些条件何时成立,您需要对Hotspot的工作原理有相当的了解。由于涉及许多非本地因素,在某些情况下依靠Hotspot明确地进行堆栈分配可能会带来风险。尤其是很难知道所有内容是否都内联。

实际上,如果仅使用简单迭代器进行迭代,则通常可以分配堆栈。对于复合对象,只能对外部对象进行堆栈分配,因此列表和其他集合总是导致堆分配。

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

除此之外,我看不到任何普遍适用的规则,这取决于代码的细节。

热点内部

首先要知道的是,转义分析是在内联之后执行的。这意味着Hotspot的转义分析在这方面比Choi论文中的描述更强大,因为从方法返回但在调用方方法本地的对象仍然可以堆栈分配。因此,如果您进行迭代,几乎可以始终为迭代器分配堆栈for(Foo item : myList) {...}myList.iterator()的实现很简单,通常是这样。)

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

内联

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

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

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

热点也只会内嵌不太大的方法。 “不太大”由-XX:MaxInlineSize=n-XX:FreqInlineSize=n选项确定。 JVM字节码大小小于MaxInlineSize的可插入方法总是内联的,如果调用“热”,则内联JVM字节码大小小于FreqInlineSize的方法。较大的方法永远不会内联。默认情况下,MaxInlineSize是35,而FreqInlineSize是依赖于平台的,但对我来说是325。因此,如果要内联,请确保您的方法不要太大。有时它可以帮助从大方法中分离出通用路径,以便可以将其内联到其调用方中。

剖析

关于性能分析的一件重要事情是,性能分析站点基于JVM字节码,而JVM字节码本身并未以任何方式内联。所以如果你有静态方法

static <T,U> List<U> map(List<T> list, Function<T,U> func) {
    List<U> result = new ArrayList();
    for(T item : list) { result.add(func.call(item)); }
    return result; 
}

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

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

标量替换

要知道的另一件事是,热点实际上并未实现对象的堆栈分配。相反,它实现了标量替换,这意味着将对象分解为其组成字段,并且像普通局部变量一样对这些字段进行堆栈分配。这意味着根本没有物体。标量替换仅在不需要创建指向堆栈分配对象的指针时才有效。某些形式的堆栈分配,例如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能够进行标量处理的变体的运行速度明显快于不能进行标定的变体。我验证了生成的机器代码(instructions),以确保Hotspot没有进行任何意外的优化。如果hotspot能够标量替换分配,那么它还可以在循环上进行一些其他优化,展开一些迭代,然后将这些迭代组合在一起。因此,在标量版本中,每个迭代器执行多个源代码级迭代的工作时,有效循环数会减少。因此,速度差异不仅是由于分配和垃圾回收开销造成的。

观察结果

我在上述程序上尝试了多种变体。标量替换的一个条件是,不得将对象分配给对象(或静态)字段,并且也不得将其分配给数组。所以在像这样的代码中

Foo f = new Foo();
bar.field = f;

Foo对象不能被标量替换。即使bar本身被标量替换,也不再使用bar.field,这仍然适用。因此,只能将对象分配给局部变量。

仅仅这还不够,Hotspot还必须能够在JIT时静态确定哪个对象实例将成为调用的目标。例如,使用footest的以下实现并删除field会导致堆分配:

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仍会标化分配。

Hotspot似乎无法将对象移动到先前已被标量替换的堆中(至少在不进行优化的情况下)。标量替换是全有或全无的事情。因此,在原始的test方法中,Scalarization的两个分配总是在堆上发生。

有条件的

一个重要的细节是,Hotspot将根据其配置文件数据预测条件。如果从不执行条件分配,则Hotspot将在该假设下编译代码,然后可能能够进行标量替换。如果在以后的某个时间确实采取了这种条件,则Hotspot将需要使用此新假设重新编译代码。由于Hotspot无法再静态确定后续调用的接收者实例,因此新代码将不进行标量替换。

例如,在test的此变体中:

static long limit = 0;

static long test() {
    long ctr = 0x5;
    long i = limit;
    limit += 0x10000;
    for(; i<limit; i++) { // In this form if scalarization happens is nondeterministic: if the condition is hit before profiling starts scalarization happens, else not.

        Scalarization s = new Scalarization();
        ctr = s.foo(ctr);
        if(i == 0xf9a0) s = new Scalarization();
        ctr = s.foo(ctr);
    }
    return ctr;
}

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

Hotspot的静态代码分析比C / C ++和其他静态编译器所能做的要局限得多,因此Hotspot在通过多个条件和其他控制结构来确定变量所引用的实例的方法中遵循控制流方面并不聪明。 ,即使对于程序员或更精巧的编译器而言,它是静态确定的。在许多情况下,配置文件信息可以弥补这一点,但这是需要注意的事情。

数组

如果在JIT时知道数组的大小,则可以堆栈分配数组。但是,除非Hotspot还可以在JIT时静态确定索引值,否则不支持索引到数组。因此,堆栈分配的数组几乎没有用。由于大多数程序不直接使用数组而是使用标准集合,因此这并不是很相关,因为嵌入式对象(例如包含ArrayList中数据的数组)由于其嵌入式性已经需要进行堆分配。我想此限制的原因是,对局部变量不存在索引操作,因此对于非常罕见的用例,这将需要其他代码生成功能。

java:继承 - java

有哪些替代继承的方法? java大神给出的解决方案 有效的Java:偏重于继承而不是继承。 (这实际上也来自“四人帮”)。他提出的理由是,如果扩展类未明确设计为继承,则继承会引起很多不正常的副作用。例如,对super.someMethod()的任何调用都可以引导您通过未知代码的意外路径。取而代之的是,持有对本来应该扩展的类的引用,然后委托给它。这是与Eric…

Java:BigInteger,如何通过OutputStream编写它 - java

我想将BigInteger写入文件。做这个的最好方式是什么。当然,我想从输入流中读取(使用程序,而不是人工)。我必须使用ObjectOutputStream还是有更好的方法?目的是使用尽可能少的字节。谢谢马丁 参考方案 Java序列化(ObjectOutputStream / ObjectInputStream)是将对象序列化为八位字节序列的一种通用方法。但…

Java DefaultSslContextFactory密钥库动态更新 - java

我有一个使用org.restlet.engine.ssl.DefaultSslContextFactory的现有应用程序和一个在服务器启动时加载的密钥库文件。我有另一个应用程序,该应用程序创建必须添加的证书服务器运行时动态地更新到密钥库文件。为此,我在代码中创建了证书和私钥,然后将其写入到目录。该目录由bash脚本监视,该脚本检查是否有新文件,如果出现,它将…

Java:线程池如何将线程映射到可运行对象 - java

试图绕过Java并发问题,并且很难理解线程池,线程以及它们正在执行的可运行“任务”之间的关系。如果我创建一个有10个线程的线程池,那么我是否必须将相同的任务传递给池中的每个线程,或者池化的线程实际上只是与任务无关的“工人无人机”可用于执行任何任务?无论哪种方式,Executor / ExecutorService如何将正确的任务分配给正确的线程? 参考方案 …

JAVA:字节码和二进制有什么区别? - java

java字节代码(已编译的语言,也称为目标代码)与机器代码(当前计算机的本机代码)之间有什么区别?我读过一些书,他们将字节码称为二进制指令,但我不知道为什么。 参考方案 字节码是独立于平台的,在Windows中运行的编译器编译的字节码仍将在linux / unix / mac中运行。机器代码是特定于平台的,如果在Windows x86中编译,则它将仅在Win…