对于某些领域中的Java应用程序,将对象/垃圾的创建量最小化是非常重要的。 这些应用程序通常无法承受GC暂停,因此它们使用特定的技术 以及避免产生垃圾的方法。 这些技术之一与迭代集合或项目数组有关。 首选方法是使用经典的for循环。 增强for循环 通过使用集合的迭代器在封面下。
为了证明这一点,我正在研究循环,因为我想更好地理解它们之间的区别,并测量使用增强型for循环创建的垃圾量, 可以说是更好,更直观的语法。
在对此进行试验之前,我(错误地?)做了一些假设:
- 在数组或集合上使用普通的for循环不会创建任何新分配在数组(?)或集合上使用增强型for循环,它确实分配通过在数组或原语集合上使用增强的for循环,通过意外地自动装填原语值,最终会以相当高的速度创建新对象
为了更好地理解这些差异,尤其是数组没有迭代器的事实,从而增强了for循环的工作原理,我按照以下步骤进行操作。
Step 1: Enhanced-for Loop Under The Cover
Enhanced-for循环只是语法糖,但是当用于数组和用于项目集合时,它实际上会产生什么结果?
The answer to this can be found in the Java Language Specification.
上述链接的主要两点是:
如果Expression的类型是Iterable的子类型,则转换如下。 If the type of Expression is a subtype of Iterable for some type argument X, then let I be the type java.util.Iterator; otherwise, let I be the raw type java.util.Iterator. The enhanced for statement is equivalent to a basic for statement of the form:
for (I #i = Expression.iterator(); #i.hasNext(); ) {
{VariableModifier} TargetType Identifier =
(TargetType) #i.next();
Statement
}
和
否则,表达式必须具有数组类型T []。 Let L1 ... Lm be the (possibly empty) sequence of labels immediately preceding the enhanced for statement. The enhanced for statement is equivalent to a basic for statement of the form:
T[] #a = Expression;
L1: L2: ... Lm:
for (int #i = 0; #i < #a.length; #i++) {
{VariableModifier} TargetType Identifier = #a[#i];
Statement
}
从上面可以看出有人确实迭代器用于集合的增强for循环。 但是,对于数组,增强的for循环只是语法糖,等效于普通的for循环。
在了解了JVM如何在不同的用例上实际实现Enhanced-for循环之后,我们的假设发生了变化:
- 使用普通的for循环遍历数组或集合不创建任何新分配在上使用增强的for循环数组它确实不创建任何新分配使用增强的for循环采集它确实allocate在上使用增强的for循环数组 or a 采集 of primitives, by accidentally autoboxing the primitive value, it ends up in a pretty high rate of new objects creation
Step 2: Defining The Test
In order to test the different scenarios I have created a very simple test which can be seen here
测试本身非常简单,需要注意的要点是:
- 测试创建一个静态的数组和一个静态的数组列表andpre-populatesthemwith100,000integers.Inthecaseofthearray,thoseareprimitives,butinthecaseofthecollectionasweuseplain数组列表thoseareactual整数objectsThetestexecutesthedifferentforloopexamplescenarios1,000,000timesThememoryusedisreadbeforetheiterationsstartandiscomparedthroughouttheexecution(every100invocations)oftheprograminordertodetermineifthememoryprofilehaschangedThetestscenariosinclude:AforloopoveranarrayAnenhanced-forloopoveranarrayAnenhanced-forloopoveranarray,byalsoautoboxingtheelementsAforloopoveracollectionAnenhanced-forloopoveracollectionAniteratorbasedforloopoveracollection,replicatingthebehaviourofenhanced-forloop'ssyntacticsugar
Step 3: Running The Test
我们使用以下设置运行测试:
- 作业系统:MacOSCatalina(10.15.3),Corei5@2.6Hz,8GBDDR3JDK:openjdk版本“13.0.2”2020-01-14JVM_OPTS:-Xms512M-Xmx512M-XX:+UnlockExperimentalVMOptions-XX:+UseEpsilonGC
We use EpsilonGC in order to eliminate any garbage collection and let the memory to just increase.
在运行测试时,可以根据我们的期望轻松地验证某些场景:
- 数组或集合上的for循环不会创建任何分配数组上的增强型for循环不会创建任何分配一个具有自动装箱功能的增强型for循环数组,它确实在创建新对象
但是,其余场景以及假设集合的增强型循环将分配新的迭代器通过使用上面的JVM属性运行上面的测试无法证明每个循环上的响应。 无论 内存配置文件稳定。 堆上没有新分配。
研究的第一步是确保字节码指示创建了新对象。 以下是字节码,可用于验证第5行是否正在进行获取迭代器的调用:
private static long forEachLoopListIterator();
Code:
0: lconst_0
1: lstore_0
2: getstatic #5 // Field LIST_VALUES:Ljava/util/List;
5: invokeinterface #9, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
10: astore_2
11: aload_2
12: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
17: ifeq 39
20: lload_0
21: aload_2
22: invokeinterface #11, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
27: checkcast #8 // class java/lang/Integer
30: invokevirtual #4 // Method java/lang/Integer.intValue:()I
33: i2l
34: ladd
35: lstore_0
36: goto 11
39: lload_0
40: lreturn
As we are using an 一种rrayList
the next step is to see what the call to #iterator()
is doing. It is indeed creating a new iterator object as can be seen in 一种rrayList source code
public Iterator<E> iterator() {
return new Itr();
}
综上所述,使用稳定的内存配置文件获得的结果没有多大意义。 肯定还有其他事情。 可能是测试错误(即JIT删除了一些代码,因为从不使用该块的返回值)。 这不应该发生,因为使用循环的所有方法的返回值都被用来对程序做进一步的决定,因此必须执行循环。
My final thinking was the 'unlikely' scenario that the objects were been placed on the stack. It is known that Hotspot performs this kind of optimizations, by using the output of Escape Analysis.
To be honest I have never seen it happening (or at least I never had the actual time to verify it was indeed happening) until now.
Step 4: Running Without Escape Analysis
验证以上假设(逃逸分析已送入JIT并导致对象在堆栈上分配)的最简单,最快的方法是关闭逃逸分析。 这个可以 通过添加完成-XX:-DoEscapeAnalysis在我们的JVM选项中。
的确,这次再次运行相同的测试,我们可以看到针对集合的增强型for循环的内存配置文件正在稳定增长。 的迭代器对象,从ArrayList#iterator() 在每个循环的堆上分配。
Conclusion
至少对我自己来说,上述发现很有趣。 在很多情况下,主要是由于时间紧缺,我们只是作一些假设并凭经验遵循“已知有效”的做法。 特别是对于那些 在面向交付的环境中工作,没有足够的钱进行一些研究,我认为这很正常。 有趣的是,不时进行一些研究并试图证明或更好地理解这一点。
最后,值得一提的是,上述行为是在实验中观察到的,而不是在实际代码中观察到的。 我可以想象生产系统中的大多数情况不会表现出这种行为(即在堆栈上分配),但是 JIT是一款如此复杂的软件,这一事实令人鼓舞,因为它可以在不实现额外收益的情况下主动优化代码。
This post is also hosted on my personal blog nikoskatsanos.com