众所周知,有时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的工作方式总是很好。
答案 0 :(得分:5)
如果是内存问题,为什么不一直使用池?
一直汇集所有内容比有时失败的简单缓存更昂贵:您需要更复杂的数据结构(可以使用简单的小数组来汇集小整数)和算法,并且您必须始终通过检查即使游泳池不会帮助你。此外,你会聚集许多永远不需要的对象,这会浪费内存:你需要更多(无用的)缓存条目,你需要管理那个缓存(或者让无用的对象活着)。
如果它不是内存问题 - 为什么要使用它?
是内存问题。此优化可以节省大量内存。汇集每个对象并不一定会减少内存使用,因为并非每个对象都被大量使用。这是一个折衷。所采用的方法为一些常见用例节省了大量内存,而不会过度减慢其他操作或浪费大量内存。
答案 1 :(得分:3)
有各种考虑因素。例如内存使用,还有语言语义。
new Integer(1);
是一个显式的对象创建。用“get from pool”运算符替换它会改变语言语义。
Integer.valueOf(1)
是显式的“从池中取出,如果在池范围内”。请注意,池是静态的,并且使用Java实现,而不是在虚拟机中实现。您可以查找它:java.lang.Integer$IntegerCache
。我认为Java规范说通过插入int
调用来建立从Integer
到Integer.valueOf
的强制转换。
现在,如果您查看如何实现此缓存,您会注意到缓存大小有一个用户可调参数。默认情况下,此缓存只包含一个数组Integer[256]
,保留-128..127
的预初始化副本(即一个字节的值范围)。显然,这个值范围为常见用途提供了最佳的性能/内存权衡。
参见例如http://martykopka.blogspot.de/2010/07/all-about-java-integer-cache.html了解更多详情。
如果您花一些时间在Java中进行数值计算,那么做会感受到自动装箱的负面影响。对于高性能数字,第一个经验法则是避免任何类型的自动装箱。例如,GNU Trove为原始类型提供了哈希映射和类似结构,运行时和内存的好处是巨大的。
Integer
对象占用16个字节的内存 - 是int
的4倍。因此,例如,高于缓存占用大约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
类型的任何魔术处理。它只是一个包裹的基元数组。当然,您希望每个类只存储一次唯一的字符串,以减少类的大小,从而减少加载时间。
答案 2 :(得分:2)
速度问题,每次分配一个新的Integer
都会耗费时间和内存。但同样的道理,在启动时分配启动过多会占用大量的内存和时间。
可悲的是,它会导致你发现的一些违反直觉的行为。
结果是我们有这种奇怪的妥协。 Java标准中讨论了此行为的原因。 (5.7)
如果被装箱的值p为真,假,一个字节,范围为\ u0000到\ u007f的字符,或者介于-128和127之间的整数或短数,则让r1和r2为任意结果p的两次拳击转换。始终是r1 == r2的情况。 理想情况下,装箱给定的原始值p将始终产生相同的参考。实际上,使用现有的实现技术可能不可行。上述规则是一种务实的妥协。上面的最后一个条款要求将某些常见值装入无法区分的对象中。实施可能会懒惰地或急切地缓存这些。
对于其他值,此公式不允许对程序员的盒装值的身份进行任何假设。这将允许(但不要求)共享部分或全部这些引用。
这确保了在大多数情况下,行为将是所需的行为,而不会造成过度的性能损失,尤其是在小型设备上。例如,较少内存限制的实现可以缓存所有字符和短路,以及-32K - + 32K范围内的整数和长整数。
<强> TL;博士强>
让它完美地运作是不可能的,根本不让它工作太奇怪了。所以我们让它在“大部分时间”都有效。
答案 3 :(得分:1)
这是记忆和速度的一个例子。当JVM启动时,你真的不想创建40亿个Integer对象,因为大多数时候它们永远不会被使用。
对于字符串,还存在在实习池中找到匹配字符串的问题。
源代码中的字符串文字很容易,因为编译器可以找到并安排实习它们,但如果我在运行时动态创建字符串,JVM必须通过池并搜索每个字符串以查看它是否匹配并且可以被重用和字符串可以
虽然有数据结构可以帮助加快速度,但创建新对象通常更简单,更快捷。
答案 4 :(得分:1)
请参阅以下代码段:
Integer i1=1;
Integer i2=2;
String s1="String";
String s2="String";
i1
,i2
仅引用不是对象,它们为booth分配了对象1
。
同样的方式s1
,s2
仅引用不是对象,它们会为对象分配对象"String"
。
而在以下代码中:
Integer i3=new Integer(1);
String s3=new String("String");
new
运算符会创建new
Object
。
希望我清除你。