Attila Szegedis 在他讲述 JVM 知识的文档中一直强调,清楚知道内存中存储的数据量是很是重要的。我一开始感到十分惊讶,由于通常状况下,在企业开发中并非常常须要关注对象的大小。他对此给出了 Twitter 的一个例子。java
先思考一个内存占用的问题:字符串 “Hello World” 会占用多少字节内存?数据库
答案:在 32 位虚拟机上是 62 字节,在 64 位虚拟机上是 86 字节。数组
分别为 8/16 (字符串的对象头) + 11 * 2 (字符) + [8/16 (字符数组的对象头) + 4 (数组长度),加上字节对齐所需的填充,共为 16/24 字节] + 4 (偏移) + 4 (偏移长度) + 4 (哈希码) + 4/8 (指向字符数组的引用)【在 64 位虚拟机上,String 对象的内存占用会由于字节对齐而填充为 40 字节】bash
假如如今有许多推特消息的地点信息须要存储。app
地点信息对应的类也许会像这样实现。ide
class Location { String city; String region; String countryCode; double long; double lat; }
很明显的一点,当加载地点信息时,其实是加载了许多的字符串,而以 Twitter 的用户规模,确定有许多字符串是重复的。按照 Attila 的说法,即便是 32 GB 大小的堆,也放不下全部数据。如今的问题是:可以经过什么方法来减小内存的占用,从而全部数据都能被加载进内存中?工具
咱们先来看两个解决方案,它们二者是相辅相成的。oop
能够看出,在地点类所存储的信息里,总有一部分是重复的,因此能够很简单地以非技术手段解决这个问题。咱们能够把地点类拆分红下面的两个类:性能
class SharedLocation { String city; String region; String countryCode; } class Location { SharedLocation sharedLocation; double long; double lat; }
由于不多有城市会改变所在的地区和国家,因此这个简单的方法可以起做用。这些字符串的组合是惟一的。这种方法也很灵活,因此也可以进行处理上面所提惟一性不知足的状况。特别是对于用户输入的地点信息,这点显得更加剧要。这样子的话,若是多条 Twitter 消息是来自同一个地点,例如 “Solingen, NRW, DE” (DE 指德国,NRW 为德国北莱茵邦,Solingen 与以后的 Ratingen 为德国城市名,译者注)的话,也只须要使用一个 SharedLocation 对象。优化
可是,其它的信息,如 “Ratingen, NRW, DE”,仍然须要在内存中存储额外的 3 个字符串,而不是单独的一个 “Ratingen”。上面的方法可使内存中的数据总量降低到 20 GB。
可是在不想或者不可以修改数据类的状况下怎么办呢?又或者是 Twitter 的那些人并无 20 GB 大小的堆。这种状况下可使用 intern() 方法,它可以使内存中的不一样字符串都只有一个实例对象。对于 intern() 方法,存在着许多误解。许多人会问道,intern() 方法是否是能够在字符串进行等价比较时,提升效率,毕竟在使用 intern 时,相等的字符串实际上都是同一个对象。确实如此,intern 能够作到这一点。(对于其余的任何对象来讲,这个规律也是成立的。)(在进行 equals 比较时,若是两个对象是同一个的话,在 “==” 比较时就能得出结果,因此能够提升 equals 比较的效率,而无论比较的对象是字符串仍是其余类型的对象,译者注。)
// java.lang.String public boolean equals(Object anObject) { if (this == anObject) { return true; } //... }
但在等价比较上的性能提高并非应该使用 intern 的理由。实际上,intern 的目的在于复用字符串对象以节省内存。
在明确知道一个字符串会出现屡次时才使用 intern(),而且只用它来节省内存。
使用 intern() 方法的效率,取决于重复的字符串与惟一的字符串的比值。另外,还要看在产生字符串对象的地方,代码是否是容易进行修改。
intern() 方法须要传入一个字符串对象(已存在于堆上),而后检查 StringTable 里是否是已经有一个相同的拷贝。StringTable 能够看做是一个 HashSet,它将字符串分配在永久代上。StringTable 存在的惟一目的就是维护全部存活的字符串的一个对象。若是在 StringTable 里找到了可以找到所传入的字符串对象,那就直接返回它,不然,把它加入 StringTable :
// OpenJDK 6 code JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper("JVM_InternString"); JvmtiVMObjectAllocEventCollector oam; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); oop result = StringTable::intern(string, CHECK_NULL); return (jstring) JNIHandles::make_local(env, result); JVM_END oop StringTable::intern(Handle string_or_null, jchar* name,int len, TRAPS) { unsigned int hashValue = hash_string(name, len); int index = the_table()->hash_to_index(hashValue); oop string = the_table()->lookup(index, name, len, hashValue); // Found if (string != NULL) return string; // Otherwise, add to symbol to table return the_table()->basic_add(index, string_or_null, name, len, hashValue, CHECK_NULL); }
所以,相同字符串的对象只会有一个。
intern 适合用在须要读取数据并将这些对象或者字符串归入一个更大范围做用域的状况。须要注意的是,硬编码在代码中的字符串(例如常量等等)都会被编译器自动的执行 intern 操做。
看一个例子:
String city = resultSet.getString(1); String region = resultSet.getString(2); String countryCode = resultSet.getString(3); double city = resultSet.getDouble(4); double city = resultSet.getDouble(5); Location location = new Location(city.intern(), region.intern(), countryCode.intern(), long, lat); allLocations.add(location);
全部新建立的地点对象都会使用 intern 获得的字符串。而从数据库读取到的临时字符串则会被垃圾回收。
最好的方法是对整个堆执行一次堆转储。堆转储也会在发生 OutOfMemoryError 时执行。
在 MAT (内存分析工具,译者注)中打开转储文件,而后选择 java.lang.String,依次点击“Java Basics”、“Group By Value”。
根据堆的大小,上面的操做可能耗费比较长的时间。最后能够看到类型这样的结果。按 “Retained Heap” 或者是 “Objects” 列进行排序,能够发现一些有趣的东西:
从这快照中咱们能够看到,空的字符串占用了大量的内存!两百万个空字符串对象占用了总共 130 MB 的空间。另外能够看到一部分被加载的 JavaScript 脚本,一些做为键的字符串,它们被用于定位。另外,还有一些与业务逻辑相关的字符串。
这些与业务逻辑相关的字符串是最容易进行 intern 操做的,由于咱们清楚地知道它们是在什么地方被加载进内存的。对于其余字符串,能够经过 “Merge shortest Path to GC Root” 选项来找到它们被存储的位置,这个信息也许可以帮助咱们找到该使用 intern 的地方。
既然 intern() 方法有这些好处,为何不常用呢?缘由在于它会下降代码效率。下面给出一个例子:
private static final int MAX = 40000000; public static void main(String[] args) throws Exception { long t = System.currentTimeMillis(); String[] arr = new String[MAX]; for (int i = 0; i < MAX; i++) { arr[i] = new String(DB_DATA[i % 10]); // and: arr[i] = new String(DB_DATA[i % 10]).intern(); } System.out.println((System.currentTimeMillis() - t) + "ms"); System.gc(); System.out.println(arr[0]); }
代码中使用了字符串数组来维护到字符串对象的强引用,另外咱们还打印了数组的第一个元素来避免数组因为代码优化而将数组给销毁了。接着从数据库加载 10 个不一样的字符串,但在这里我使用了 new String() 来建立一个临时的字符串,这和从数据库里读是同样的。最后咱们调用了系统的 GC() 方法,这样就能排除其余不相关对象的影响,保证结果的正确。 在 64 位,8 G 内存,i5-2520M 处理器的 Windows 系统上运行上面的代码, 环境为 JDK 1.6.0_27,指定虚拟机参数 -XX:+PrintGCDetails -Xmx6G -Xmn3G 记录垃圾回收日志。结果以下:
没有使用 intern() 方法的结果:
1519ms [GC [PSYoungGen: 2359296K->393210K(2752512K)] 2359296K->2348002K(4707456K), 5.4071058 secs] [Times: user=8.84 sys=1.00, real=5.40 secs] [Full GC (System) [PSYoungGen: 393210K->392902K(2752512K)] [PSOldGen: 1954792K->1954823K(1954944K)] 2348002K->2347726K(4707456K) [PSPermGen: 2707K->2707K(21248K)], 5.3242785 secs] [Times: user=3.71 sys=0.20, real=5.32 secs] DE Heap PSYoungGen total 2752512K, used 440088K [0x0000000740000000, 0x0000000800000000, 0x0000000800000000) eden space 2359296K, 18% used [0x0000000740000000,0x000000075adc6360,0x00000007d0000000) from space 393216K, 0% used [0x00000007d0000000,0x00000007d0000000,0x00000007e8000000) to space 393216K, 0% used [0x00000007e8000000,0x00000007e8000000,0x0000000800000000) PSOldGen total 1954944K, used 1954823K [0x0000000680000000, 0x00000006f7520000, 0x0000000740000000) object space 1954944K, 99% used [0x0000000680000000,0x00000006f7501fd8,0x00000006f7520000) PSPermGen total 21248K, used 2724K [0x000000067ae00000, 0x000000067c2c0000, 0x0000000680000000) object space 21248K, 12% used [0x000000067ae00000,0x000000067b0a93e0,0x000000067c2c0000)
使用了 intern() 方法的结果:
1519ms [GC [PSYoungGen: 2359296K->393210K(2752512K)] 2359296K->2348002K(4707456K), 5.4071058 secs] [Times: user=8.84 sys=1.00, real=5.40 secs] [Full GC (System) [PSYoungGen: 393210K->392902K(2752512K)] [PSOldGen: 1954792K->1954823K(1954944K)] 2348002K->2347726K(4707456K) [PSPermGen: 2707K->2707K(21248K)], 5.3242785 secs] [Times: user=3.71 sys=0.20, real=5.32 secs] DE Heap PSYoungGen total 2752512K, used 440088K [0x0000000740000000, 0x0000000800000000, 0x0000000800000000) eden space 2359296K, 18% used [0x0000000740000000,0x000000075adc6360,0x00000007d0000000) from space 393216K, 0% used [0x00000007d0000000,0x00000007d0000000,0x00000007e8000000) to space 393216K, 0% used [0x00000007e8000000,0x00000007e8000000,0x0000000800000000) PSOldGen total 1954944K, used 1954823K [0x0000000680000000, 0x00000006f7520000, 0x0000000740000000) object space 1954944K, 99% used [0x0000000680000000,0x00000006f7501fd8,0x00000006f7520000) PSPermGen total 21248K, used 2724K [0x000000067ae00000, 0x000000067c2c0000, 0x0000000680000000) object space 21248K, 12% used [0x000000067ae00000,0x000000067b0a93e0,0x000000067c2c0000)
能够看到结果差异十分的大。在使用 intern() 方法的时候,程序耗时多了 3 秒,但节省了很大一块内存。使用 intern() 方法的程序占用了 253472K(250M) 内存,而不使用的占用了 2397635K (2.4G)。从这些能够看出使用 intern 的利弊。