每个Java开发人员都知道,Java中有八种原始类型:
原始类型代表了用代码表示数据的最简单,最直接的方法。即使 Java 中最复杂的类也可以简化为它们所表示的原始数据类型集。但是原始类型不是对象,这带来了一个问题。
例如, JDK 中的所有收集类都将数据作为对象保存。如果开发人员有一组要存储在 ArrayList 中的 int 值,则无法完成。当然,除非他们使用相应的包装器类或利用 Java 中的自动装箱功能。
对于每种基本类型,都有一个对应的包装器类:
从历史上看,如果开发人员想在集合类中存储一组 double ,则必须将其从原始类型转换为包装类:
int x = 10;
ArrayList<E> list = new ArrayList();
// list.add(10); Pre JDK 1.5 autoboxing would not work
Integer wrapper = Integer.valueOf(x);
list.add(wrapper);
但是, JDK 1.5 版引入了一项称为Java原语类型自动装箱的功能。这意味着在较新的 JDK 上,当在需要引用类型的任何地方使用基本类型时,将自动创建包装器类。因此,在 JDK 1.5 之后的 JVM 版本上,上述用例无需使用包装器类。Java 装箱和原始类型的自动装箱将为您处理:
int x = 10;
ArrayList<E> list = new ArrayList();
list.add(10); // This is primitive type autoboxing in Java
//Integer wrapper = Integer.valueOf(x);
//list.add(wrapper);
我一直以为,当Java引入原始类型装箱和装箱时,也实现了JVM级别的优化,以解决与Java自动装箱相关的任何性能问题。我认为在时钟周期,垃圾回收和内存消耗方面,在包装器类和原始类型之间移动是相对平稳的操作。
我不可能错得更多。
这是高度人为设计的用例,其灵感主要来自Marcus Hirt的 JMC示例。
首先,我们创建一个简单的组件,用作int值的包装器类。该类名为 SnoopInt :
package com.mcnz.jfr.jmc;
public final class SnoopInt {
final int id;
SnoopInt(int id) { this.id = id; }
int getId() { return id; }
}
然后,我们有了一个可运行的类,该类将一百万个原始类型的 int 值推入映射。
然后,我们复制地图中的所有值,然后遍历原始地图以确认副本中的所有值也都在原始文件中。这是一个人为的示例,但是它给JVM带来了负担,并且在垃圾回收和内存性能指标方面产生了一些有趣的结果。
有很多自动装箱的 Java 基本类型,因此我将类命名为 MikeTyson :
package com.mcnz.jfr.jmc;
import java.util.*;
public final class MikeTyson implements Runnable {
private final Map<Integer, SnoopInt> map = new HashMap<>();
public MikeTyson() {
for (int i = 0; i < 1_000_000; i++) {
map.put(i, new SnoopInt(i));
}
}
public void run() {
long yieldCounter = 0;
while (true) {
Collection copyOfValues = map.values();
for (SnoopInt snoopIntCopy : copyOfValues) {
if (!map.containsKey(snoopIntCopy.getId()))
System.out.println("Now this is strange!");
if (++yieldCounter % 1000 == 0)
System.out.println("Boxing and unboxing");
Thread.yield();
}
}
}
public static void main(String[] args) throws java.io.IOException {
ThreadGroup threadGroup = new ThreadGroup("Workers");
Thread[] threads = new Thread[8];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(threadGroup, new MikeTyson(), "Allocator Thread " + i);
threads[i].setDaemon(true);
threads[i].start();
}
System.out.print("Press to quit!");
System.out.flush();
System.in.read();
}
}
使用 Java Flight Recorder 和 JDK Mission Control Eclipse 插件对该程序进行快速分析会触发红色警告,将“原始到对象转换”标记为有问题。自动装箱导致性能问题。
Java 原语类型的装箱和拆箱会导致 JVM 性能问题。
此外,当您检查 Java Mission Control 的垃圾收集指标时,您会发现垃圾收集不在图表中:
当使用自动装箱功能时,Java Mission Control 显示了猖 ramp 的垃圾回收例程会影响性能。
您如何解决 Java 自动装箱性能问题?
开发人员只需更改几行代码即可解决该问题。如果在整个应用程序中使用 Integer 引用类型,则所有垃圾回收问题都将消失。
MikeTyson 类的构造函数有一个小的变化:
public MikeTyson() {
for (int i = 0; i < 700_000; i++) {
map.put(Integer.valueOf(i),
new SnoopInt(Integer.valueOf(i)));
}
}
并且自定义包装器类也将更新为也使用 Integer 引用类型:
public final class SnoopInt {
final Integer id;
SnoopInt(Integer id) { this.id = id; }
Integer getId() { return id; }
}
进行了这些较小的更改之后,再次启动 Java Flight Recorder,Java 基本类型装箱和拆箱性能问题就消失了。垃圾回收没有明显增加,并且在 Java Flight Recorder 运行之后, Java Mission Control 不会报告任何从原语到对象的转换问题。
我一直认为自动装箱 Java 对性能的影响很小,但是我还是错了。性能影响可能很大。
这就是为什么使用 Java 的 JVM Flight Recorder 和 JDK Mission Control 工具之类的工具不断地分析应用程序的重要性。假设还不错,只要存在一种验证它们的机制即可,正如此 Java 自动装箱性能示例清楚地表明的那样。