Attila Szegedis 在他讲述 JVM 知识的文档中一直强调,清楚知道内存中存储的数据量是非常重要的。我一开始感到十分惊讶,因为一般情况下,在企业开发中并不是经常需要关注对象的大小。他对此给出了 Twitter 的一个例子。

先思考一个内存占用的问题:字符串 “Hello World” 会占用多少字节内存?

答案:在 32 位虚拟机上是 62 字节,在 64 位虚拟机上是 86 字节。

分别为 8/16 (字符串的对象头) + 11 * 2 (字符) + [8/16 (字符数组的对象头) + 4 (数组长度),加上字节对齐所需的填充,共为 16/24 字节] + 4 (偏移) + 4 (偏移长度) + 4 (哈希码) + 4/8 (指向字符数组的引用)【在 64 位虚拟机上,String 对象的内存占用会因为字节对齐而填充为 40 字节】

假如现在有许多推特消息的地点信息需要存储。

地点信息对应的类也许会像这样实现。

class Location {String city;String region;String countryCode;double long;double lat;
}

很明显的一点,当加载地点信息时,实际上是加载了许多的字符串,而以 Twitter 的用户规模,肯定有许多字符串是重复的。按照 Attila 的说法,即使是 32 GB 大小的堆,也放不下所有数据。现在的问题是:能够通过什么方法来减少内存的占用,从而所有数据都能被加载进内存中?

我们先来看两个解决方案,它们两者是相辅相成的。

Attilas 提出的方法

可以看出,在地点类所存储的信息里,总有一部分是重复的,所以可以很简单地以非技术手段解决这个问题。我们可以把地点类拆分成下面的两个类:

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。

使用 String intern() 方法

但是在不想或者不能够修改数据类的情况下怎么办呢?又或者是 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() 方法需要传入一个字符串对象(已存在于堆上),然后检查 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_ENDoop 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);// Foundif (string != NULL) return string;// Otherwise, add to symbol to tablereturn 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 得到的字符串。而从数据库读取到的临时字符串则会被垃圾回收。

如何确定 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 的利弊

既然 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
HeapPSYoungGen      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() 方法的结果:

4838ms
[GC [PSYoungGen: 2359296K->156506K(2752512K)] 2359296K->156506K(2757888K), 0.1962062 secs] [Times: user=0.69 sys=0.01, real=0.20 secs]
[Full GC (System) [PSYoungGen: 156506K->156357K(2752512K)] [PSOldGen: 0K->18K(5376K)] 156506K->156376K(2757888K) [PSPermGen: 2708K->2708K(21248K)], 0.2576126 secs] [Times: user=0.25 sys=0.00, real=0.26 secs]
DE
HeapPSYoungGen      total 2752512K, used 250729K [0x0000000740000000, 0x0000000800000000, 0x0000000800000000)eden space 2359296K, 10% used [0x0000000740000000,0x000000074f4da6f8,0x00000007d0000000)from space 393216K, 0% used [0x00000007d0000000,0x00000007d0000000,0x00000007e8000000)to   space 393216K, 0% used [0x00000007e8000000,0x00000007e8000000,0x0000000800000000)PSOldGen        total 5376K, used 18K [0x0000000680000000, 0x0000000680540000, 0x0000000740000000)object space 5376K, 0% used [0x0000000680000000,0x0000000680004b30,0x0000000680540000)PSPermGen       total 21248K, used 2725K [0x000000067ae00000, 0x000000067c2c0000, 0x0000000680000000)object space 21248K, 12% used [0x000000067ae00000,0x000000067b0a95d0,0x000000067c2c0000

可以看到结果差别十分的大。在使用 intern() 方法的时候,程序耗时多了 3 秒,但节省了很大一块内存。使用 intern() 方法的程序占用了 253472K(250M) 内存,而不使用的占用了 2397635K (2.4G)。从这些可以看出使用 intern 的利弊。

原文链接: Fabian Lange 翻译: ImportNew.com - chowchowT^T
译文链接: http://www.importnew.com/21836.html
[ 转载请保留原文出处、译者和译文链接。]

使用String的intern方法节省内存相关推荐

  1. Java之String系列--intern方法的作用及原理

    原文网址:Java之String系列--intern方法的作用及原理_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java的String的intern方法的原理. 常量池简介 在 JAVA 语言中 ...

  2. Java String的intern方法

    1. 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ne ...

  3. [转]String 之 new String()和 intern()方法深入分析

    引入 String,是 Java 中除了基本数据类型以外,最为重要的一个类型了.很多人会认为他比较简单.但是和 String 有关的面试题有很多,下面我随便找两道面试题,看看你能不能都答对: Q1:S ...

  4. String中intern()方法

    本文参考 https://blog.csdn.net/believesoul/article/details/79588305 ,我对这篇文章进行了一些小小的整理和修改.这篇文章很好的对intern( ...

  5. String的intern方法演示及各种字符串的拼接对比

    演示String的intern方法,以及各种拼接字符串的区别 package string;/*** @Author gzx* @create 2022-1-14 jdk8*/ public clas ...

  6. String中intern方法的作用

    前言 读完这篇文章你可以了解,String对象在虚拟机内存中的存放,intern的作用,这么多String对象的创建到底有什么区别,String 创建的对象有几个!! 进入正题 先科普几个知识点 1. ...

  7. JDK8中String的intern()方法详细解读【内存图解+多种例子+1.1w字长文】

    文章目录 一.前言 二.图文理解String创建对象 1.例子一 2.例子二 3.例子三 4.例子四 5.例子五 6.例子六 三.深入理解intern()方法 1. 源码查看 2. 例子一 3. 例子 ...

  8. java intern_java String的intern方法

    我们知道再jvm的运行时内存可以分为堆.方法区.程序计数器.虚拟机栈和本地方法栈.而在方法区中有一个字符串常量池,用来保存字符串这个不可变量.如果我们使用String str=new String(& ...

  9. 字符斜杠是合法常量吗_【面试秘籍】你对String的intern方法了解吗

    我们先来看个例子: public class StringTest { public static void main(String[] args) { String a = "A" ...

最新文章

  1. Android SQLite数据库之事务的学习
  2. mysql锁表_MySQL中Alter table 你不知道的性能问题
  3. 百度上线独立视频App百度看看,能搜到微视、B站内容?
  4. 梆梆加固还原DEX文件
  5. 【论文串烧】基于特定实体的文本情感分类总结(PART I)
  6. hdu 1217 Arbitrage (最小生成树)
  7. cgroup学习(七)——cpu子系统
  8. pyhive ModuleNotFoundError: No module named ‘thrift‘
  9. android系统查看wifi密码,安卓手机如何查看WIFI密码
  10. Msm8960(APQ8064)平台的MSM-AOSP-kitkat编译适配(8):wifi与蓝牙
  11. 进程ld-linux-x86-64是什么,解决挖矿病毒占用cpu以及误删 ld-linux-x86-64.so.2 文件的问题...
  12. 5分钟学会图片优化 之 七伤拳
  13. nginx 屏蔽某些ip,防止有些人刷接口攻击
  14. 基于深度卷积神经网络的图像超分辨率重建(SRCNN) 学习笔记
  15. Python3 微信支付(小程序支付)V3接口
  16. 使用dnsmasq,实现本地的DNS服务
  17. 前端学习日记2-html表单元素
  18. [Zookeeper-3.6.2源码解析系列]-14-Zookeeper使用到的Reactor网络模型原理分析
  19. 超市结算系统软件测试,软件测试报告-超市管理系统【参考】.doc
  20. 跨越逐梦路上的荆棘(程序猿生存指南)

热门文章

  1. 由路由器AP隔离引起的WEB服务不能访问的问题
  2. 安卓与IOS的H5差异性
  3. 游戏手机低迷,黑鲨、红魔花式求生
  4. SyntaxError: Unexpected end of JSON input (file: C:\Users\admin\Desktop\...)
  5. bzoj1132 [POI2008]Tro(叉积)
  6. linux环境下安装idk
  7. UVa 10697 - Firemen barracks
  8. Photos(PHAsset)
  9. 速看!你的反链不一定有用!
  10. 抄袭,腾讯 和 产品