7

众所周知,Java 有时将对象池用于包装器和字符串类型,有时则不会。

例如:

Integer i1 = 1;
Integer i2 = 1;
Integer i3 = new Integer(1);
String s1 = "String";
String s2 = "String";
String s3 = new String ("String");
System.out.println("(i1 == i2) " + (i1 == i2));
System.out.println("(i2 == i3) " + (i2 == i3));
System.out.println("(s1 == s2) " + (s1 == s2));
System.out.println("(s2 == s3) " + (s2 == s3));

Execution result:

(i1 == i2) true
(i2 == i3) false
(s1 == s2) true
(s2 == s3) false

正如您所见,原语装箱从池中获取对象,通过字符串文字创建字符串也从池中获取对象。这样的对象实际上是同一个对象(operator ==返回 true )。

创建包装器和字符串的其他机制不会从池中获取对象。以这些方式创建的对象实际上是不同的对象(运算符==在它们上返回 false)。

让我感到困惑的是游泳池被部分使用的事实。

如果是内存问题,为什么不一直使用池?如果它不是内存问题 - 为什么要使用它?

问题是 - 实现这种行为的原因是什么(= 池的部分使用)?

这个问题相当理论化,但它有实际应用——它可以帮助理解如何正确使用自定义对象池,当然理解 Java 的工作原理总是好的。

4

5 回答 5

5

如果是内存问题,为什么不一直使用池?

始终池化所有内容比有时会失败的简单缓存更昂贵:您需要更复杂的数据结构(池化小整数可以使用简单的小数组完成)和算法,并且您必须始终进行检查,即使当游泳池帮不了你。此外,您将汇集许多不再需要的对象,这是对内存的浪费:您需要更多(无用的)缓存条目,并且您需要管理该缓存(或使无用的对象保持活动状态)。

如果它不是内存问题 - 为什么要使用它?

一个内存问题。这种优化节省了大量内存。池化每个对象并不一定会减少内存使用,因为并非每个对象都被大量使用。这是一个权衡。所采取的方法为一些常见用例节省了大量内存,而不会过度减慢其他操作或浪费大量内存。

于 2013-01-18T16:14:40.517 回答
3

有各种考虑。例如内存使用,还有语言语义。

 new Integer(1);

是一个显式的对象创建。用“从池中获取”运算符替换它会改变语言语义。

 Integer.valueOf(1)

是明确的“从池中获取,如果在池范围内”。请注意,池是静态的,并且是用 Java 实现的,而不是在虚拟机中实现的。你可以查一下:java.lang.Integer$IntegerCache. 我认为 Java 规范说从intto的转换Integer是通过插入Integer.valueOf调用来进行的。

现在,如果您查看此缓存是如何实现的,您会注意到缓存大小有一个用户可调参数。默认情况下,这个缓存只包含一个Integer[256]保存预初始化副本的数组-128..127(即一个字节的值范围)。显然,这个值范围为常见用途提供了最佳的性能/内存折衷。

有关更多详细信息,请参见例如http://martykopka.blogspot.de/2010/07/all-about-java-integer-cache.html

如果您花一些时间在 Java 中进行数值计算,您确实会感受到自动装箱的负面影响。对于高性能数字,第一条经验法则是避免任何类型的自动装箱。例如,GNU Trove 为原始类型提供了 hashmap 和类似结构,运行时和内存的好处是巨大的。

一个Integer对象占用 16 个字节的内存 - 是int. 例如,上面的缓存占用大约 5kb 的内存。这是大多数应用程序都愿意浪费的东西。但显然,你不能对所有整数都这样做!

至于字符串,编译器需要将它们适当地存储在类文件中。那么如何将字符串存储在类文件中呢?最简单的方法是将代码重写为如下所示:

private static final char[] chars_12345 = new char[]{ 't', 'e', 's', 't'};

private static final String CONST_STRING_12345 = new String(chars_12345);

它不依赖于任何String类型的魔法处理。它只是一个包装的原语数组。当然,您希望每个类只存储每个唯一字符串一次,以减少类大小,从而减少加载时间。

于 2013-01-18T16:10:01.013 回答
2

这是一个速度问题,Integer每次分配一个新的都会耗费时间和内存。但出于同样的原因,在启动时分配过多会占用大量内存和时间。

可悲的是,它会导致您发现一些违反直觉的行为。

结果就是我们有这种奇怪的妥协。Java 标准中讨论了这种行为的原因。(5.7)

如果被装箱的值 p 是真、假、一个字节、一个在 \u0000 到 \u007f 范围内的字符,或者一个介于 -128 和 127 之间的 int 或短数字,则令 r1 和 r2 为任意两次装箱转换的结果p。r1 == r2 总是如此。理想情况下,对给定的原始值 p 进行装箱将始终产生相同的参考。在实践中,使用现有的实现技术这可能是不可行的。上述规则是一种务实的妥协。上面的最后一个条款要求某些常见的值总是被装箱到无法区分的对象中。实现可能会懒惰地或急切地缓存这些。

对于其他值,此公式不允许程序员对装箱值的身份进行任何假设。这将允许(但不要求)共享部分或全部这些引用。

这确保了在最常见的情况下,行为将是期望的行为,而不会造成过度的性能损失,尤其是在小型设备上。例如,内存限制较少的实现可能会缓存所有字符和短整数,以及 -32K - +32K 范围内的整数和长整数。

tl;博士

让它完美运行是不可能的,根本不让它运行也太奇怪了。所以我们让它在“大部分”时间里工作。

于 2013-01-18T16:10:17.187 回答
1

这是内存和速度的问题。您真的不想在 JVM 启动时创建 40 亿个 Integer 对象,因为大多数时候它们永远不会被使用。

对于字符串,还存在在实习池中查找匹配字符串的问题。

源代码中的字符串文字很容易,因为编译器可以找到并安排实习它们,但如果我在运行时动态创建一个字符串,JVM 将不得不遍历池并搜索每个字符串以查看它是否匹配并且可以重用并且字符串可以

虽然有一些数据结构可以帮助加快速度,但创建新对象通常更简单、更快捷。

于 2013-01-18T16:11:00.230 回答
1

看,下面的代码段:

Integer i1=1;
Integer i2=2;
String s1="String";
String s2="String";

,只是引用而不是对象,它们的展位被分配了i1对象。同样的方式,只是引用而不是对象,它们的展位被分配了对象。i21s1s2"String"

而在以下代码中:

Integer i3=new Integer(1);
String s3=new String("String");

操作员new创建一个. new Object希望我清除了你。

于 2013-01-18T16:11:04.593 回答