本文将为你简单介绍一下Java 8 update 20中引入的字符串去重的特性。
从平均情况来看,应用程序中的String对象会消耗大量的内存。这里面有一部分是冗余的——同样的字符串会存在多个不同的实例(a != b, 但a.equals(b))。在实践中,有许多字符串会出于不同的原因造成冗余。
最初JDK提供了一个String.intern()方法来解决字符串冗余的问题。这个方法的缺点在于你必须得去找出哪些字符串需要进行驻留(interned)。这通常都需要一个具备冗余字符串查找功能的堆分析的工具才行,比如
Youkit profiler。如果使用得当的话,字符串驻留会是一个非常有效的节省内存的工具——它让你可以重用整个字符串对象(每个字符串对象在底层char[]的基础上会增加24字节的额外开销)。
从Java 7 update 6开始,每个String对象都有一个自己专属的私有char[] 。这样JVM才可以自动进行优化——既然底层的char[]没有暴露给外部的客户端的话,那么JVM就能去判断两个字符串的内容是否是一致的,进而将一个字符串底层的char[]替换成另一个字符串的底层char[]数组。
字符串去重这个特性就是用来做这个的,它在Java 8 update 20中被引入。下面是它的工作原理:
1. 你得使用G1垃圾回收器并启用这一特性:-XX:+UseG1GC -XX:+UseStringDeduplication。这一特性作为G1垃圾回收器的一个可选的步骤来实现的,如果你用的是别的回收器是无法使用这一特性的。
2. 这个特性会在G1回收器的minor GC阶段中执行。根据我的观察来看,它是否会执行取决于有多少空闲的CPU周期。因此,你不要指望它会在一个处理本地数据的数据分析器中会被执行。也就是说,WEB服务器中倒是很可能会执行这个优化。
3. 字符串去重会去查找那些未被处理的字符串,计算它们的hash值(如果它没在应用的代码中被计算过的话),然后再看是否有别的字符串的hash值和底层的char[]都是一样的。如果找到的话——它会用一个新字符串的char[]来替换掉现有的这个char[]。
4. 字符串去重只会去处理那些历经数次GC仍然存活的那些字符串。这样能确保大多数的那些短生命周期的字符串不会被处理。字符串的这个最小的存活年龄可以通过-XX:StringDeduplicationAgeThreshold=3的JVM参数来指定(3是这个参数的默认值)。
下面是这个实现的一些重要的结论:
没错,如果你想享受字符串去重特性的这份免费午餐的话,你得使用G1回收器。使用parellel GC的话是无法使用它的,而对那些对吞吐量要求比延迟时期高的应用而言,parellel GC应该是个更好的选择。
字符串去重是无法在一个已加载完的系统中运行的。要想知道它是否被执行了,可以通过-XX:+PrintStringDeduplicationStatistics参数来运行JVM,并查看控制台的输出。
如果你希望节省内存的话,你可以在应用程序中将字符串进行驻留(interned)——那么放手去做吧,不要依赖于字符串去重的功能。你需要时刻注意的是字符串去重是要处理你所有的字符串的(至少是大部分吧)——也就是说尽管你知道某个指定的字符串的内容是唯一的(比如说GUID),但JVM并不知道这些,它还是会尝试将这个字符串和其它的字符串进行匹配。这样的结果就是,字符串去重所产生的CPU开销既取决于堆中字符串的数量(将新的字符串和别的字符串进行比较),也取决于你在字符串去重的间隔中所创建的字符串的数量(这些字符串会和堆中的字符串进行比较)。在一个拥有好几个G的堆的JVM上,可以通过-XX:+PrintStringDeduplicationStatistics选项来看下这个特性所产生的影响究竟有多大。
另一方面,它基本是以一种非阻塞的方式来完成的,如果你的服务器有足够多的空闲CPU的话,那为什么不用呢?
最后,请记住,String.intern可以让你只针对你的应用程序中指定的某一部分已知会产生冗余的字符串。通常来说,它只需要比较一个较小的驻留字符串的池就可以了,也就是说你可以更高效地使用你的CPU。不仅如此,你还可以将整个字符串对象进行驻留,这样每个字符串你还多节省了24个字节。
这里是我用来试验这一特性的一个测试类。这三个测试都会一直运行到JVM抛出OOM为止,因此你得分别去单独地运行它们。
第一个测试会创建内容一样的字符串,如果你想知道当堆中字符串很多的时候,字符串去重会花掉多少时间的话,这个测试就变得非常有用了。尽量给第一个测试分配尽可能多的内存——它创建的字符串越多,优化的效果就越好。
第二三个测试会比较去重(第二个测试)及驻留(interning, 第三个测试)间的差别。你得用一个相同的Xmx设置来运行它们。在程序中我把这个常量设置成了Xmx256M,但是当然了,你可以分配得多点。然而,你会发现,和interning测试相比,去重测试会更早地挂掉。这是为什么?因为我们在这组测试中只有100个不同的字符串,因此对它们进行驻留就意味着你用到的内存就只是存储这些字符串所需要的空间。而字符串去重的话,会产生不同的字符串对象,它仅会共享底层的char[]数组。
/**
* String deduplication vs interning test
*/
public class StringDedupTest {
private static final int MAX_EXPECTED_ITERS = 300;
private static final int FULL_ITER_SIZE = 100 * 1000;
//30M entries = 120M RAM (for 300 iters)
private static List<String> LIST = new ArrayList<>( MAX_EXPECTED_ITERS * FULL_ITER_SIZE );
public static void main(String[] args) throws InterruptedException {
//24+24 bytes per String (24 String shallow, 24 char[])
//136M left for Strings
//Unique, dedup
//136M / 2.9M strings = 48 bytes (exactly String size)
//Non unique, dedup
//4.9M Strings, 100 char[]
//136M / 4.9M strings = 27.75 bytes (close to 24 bytes per String + small overhead
//Non unique, intern
//We use 120M (+small overhead for 100 strings) until very late, but can't extend ArrayList 3 times - we don't have 360M
/*
Run it with: -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
Give as much Xmx as you can on your box. This test will show you how long does it take to
run a single deduplication and if it is run at all.
To test when deduplication is run, try changing a parameter of Thread.sleep or comment it out.
You may want to print garbage collection information using -XX:+PrintGCDetails -XX:+PrintGCTimestamps
*/
//Xmx256M - 29 iterations
fillUnique();
/*
This couple of tests compare string deduplication (first test) with string interning.
Both tests should be run with the identical Xmx setting. I have tuned the constants in the program
for Xmx256M, but any higher value is also good enough.
The point of this tests is to show that string deduplication still leaves you with distinct String
objects, each of those requiring 24 bytes. Interning, on the other hand, return you existing String
objects, so the only memory you spend is for the LIST object.
*/
//Xmx256M - 49 iterations (100 unique strings)
//fillNonUnique( false );
//Xmx256M - 299 iterations (100 unique strings)
//fillNonUnique( true );
}
private static void fillUnique() throws InterruptedException {
int iters = 0;
final UniqueStringGenerator gen = new UniqueStringGenerator();
while ( true )
{
for ( int i = 0; i < FULL_ITER_SIZE; ++i )
LIST.add( gen.nextUnique() );
Thread.sleep( 300 );
System.out.println( "Iteration " + (iters++) + " finished" );
}
}
private static void fillNonUnique( final boolean intern ) throws InterruptedException {
int iters = 0;
final UniqueStringGenerator gen = new UniqueStringGenerator();
while ( true )
{
for ( int i = 0; i < FULL_ITER_SIZE; ++i )
LIST.add( intern ? gen.nextNonUnique().intern() : gen.nextNonUnique() );
Thread.sleep( 300 );
System.out.println( "Iteration " + (iters++) + " finished" );
}
}
private static class UniqueStringGenerator
{
private char upper = 0;
private char lower = 0;
public String nextUnique()
{
final String res = String.valueOf( upper ) + lower;
if ( lower < Character.MAX_VALUE )
lower++;
else
{
upper++;
lower = 0;
}
return res;
}
public String nextNonUnique()
{
final String res = "a" + lower;
if ( lower < 100 )
lower++;
else
lower = 0;
return res;
}
}
}
总结
Java 8 update 20中添加了字符串去重的特性。它是G1垃圾回收器的一部分,因此你必须使用G1回收器才能启用它:-XX:+UseG1GC -XX:+UseStringDeduplication。
字符串去重是G1的一个可选的阶段。它取决于当前的系统负载。
字符串去重会查询内容相同的那些字符串,并将它们底层存储字符的char[]数组进行统一。使用这一特性你不需要写任何代码,不过这意味着最后你得到的是不同的字符串对象,每个对象会占用24个字节。有的时候显式地调用String.intern进行驻留还是有必要的。
字符串去重不会对年轻的字符串进行处理。字符串处理的最小年龄是通过-XX:StringDeduplicationAgeThreshold=3的JVM参数来进行管理的(3是这个参数的默认值)。
原创文章转载请注明出处:
Java译站
英文原文链接
分享到:
相关推荐
Java中的字符串去重特性是Java 8 Update 20引入的一种优化机制,旨在减少内存消耗,特别是对于大量字符串操作的应用。这一特性依赖于G1(Garbage First)垃圾回收器,G1是Java 8中默认关闭的一个高级垃圾回收算法。...
String对象有个特殊的...而G1垃圾回收器的字符串去重的功能其实和String.intern有点不一样,G1是让两个字符串的底层指向同一个byte[]数组。 有图为证: 上图中的String1和String2指向的是同一个byte[]数组。 Strin
1. **G1中的去重操作**:G1垃圾收集器在处理StringTable时,会进行字符串去重,减少内存占用。 2. **回收效率**:由于永久代的回收频率低,将StringTable移至堆空间后,可以更及时地进行垃圾回收,适应现代Java应用...
8. **-XX:+UseStringDeduplication**: 在G1 GC中启用字符串去重,减少内存占用。 9. **-XX:+PrintFlagsFinal**: 打印所有默认的JVM设置和它们的值,有助于理解和调试JVM行为。 10. **-XX:+...
2. **字符串去重**:避免大量重复字符串占用内存。 3. **合理使用数据结构**:根据业务场景选择合适的数据结构,如使用HashMap替代ArrayList等。 4. **避免内存泄漏**:及时释放不再使用的对象引用,防止内存溢出。 ...
-XX:+UseStringDeduplication启用字符串去重,减少内存占用。 四、JVM性能监控与诊断工具 1. **JConsole**:官方提供的GUI工具,可实时监控JVM的内存、线程、CPU使用情况等。 2. **VisualVM**:更全面的分析工具,...
12. `-XX:StringDeduplicationAgeThreshold=threshold`: 字符串去重的年龄阈值,超过此阈值的字符串将尝试去重。 这些参数提供了丰富的调优选项,开发者可以根据应用的特性、性能需求以及系统资源来调整,以达到...
5. **字符串去重**:新方法`stripIndent()`和`lines()`,用于去除字符串的缩进和处理多行字符串,简化了文本处理。 6. **垃圾收集器改进**:G1垃圾收集器成为默认的垃圾收集器,提供了更好的性能和内存管理。 7. *...
字符串字面量在创建时会自动进行去重操作,从而节省内存空间。例如: ```java String str1 = "abc"; String str2 = "abc"; System.out.println(str1 == str2); // 输出true ``` 上述代码中,`str1` 和 `str2`...
- `-XX:+UseStringDeduplication` (G1 GC)启用字符串去重,减少内存占用。 10. **其他优化选项**: - `-XX:+AggressiveOpts` 开启一些激进的优化选项,但可能不稳定。 - `-XX:+UseLargePages` 使用大页内存,...
-XX:+PrintStringDeduplication输出字符串去重信息。 -XX:+PrintHeapAtSIGBREAK在接收到特定信号时打印堆信息。 -XX:+PrintClassHistogramBeforeFullGC和-XX:+PrintClassHistogramAfterFullGC分别在full GC前后打印...
- `-XX:+PrintStringDeduplicationInfo`:跟踪字符串去重操作。 - `-XX:+PrintHeapAtSIGBREAK`:在接收到特定信号时输出堆信息。 - `-XX:+PrintClassHistogram*`:打印类直方图,有助于分析内存占用。 5. **内存...
4. **字符串去重**:开启字符串常量池 `-XX:+UseStringDeduplication`,减少内存占用。 5. **JVM参数监控**:使用`-XX:+PrintFlagsFinal`查看默认JVM参数,`-XX:+PrintGCDetails`监控GC行为。 二、线程池调优 1. **...
String 类型用于存储字符串,List 类型用于存储列表,Set 类型用于存储集合,Map 类型用于存储键值对,ZSet 类型用于存储有序集合。Redis 使用 TTL 命令来解决数据过期问题。 存储过程和消息队列 15. 存储过程的...
- **`Instr`函数**: 用于查找一个字符串在另一个字符串中的位置。 ### 20. 数据类型 **知识点:** - **单精度数字类型**: 在数据库中通常占用4个字节。 ### 21. 二进制位 **知识点:** - **`bit`**: 计算机中最...
String有`length()`方法来获取字符串的长度。 【Set中的去重】 Set通过equals()方法来判断元素是否重复,不使用`==`,因为`==`比较的是对象引用,而equals()比较的是对象内容。 【接口与抽象类】 接口可以继承接口...
在Java中,使用`new String()`创建字符串会创建一个新的字符串对象,并且还会在常量池中创建一个相同的字符串副本。而直接使用`String s = "xyz";`创建的字符串只会在常量池中创建,不会额外创建对象。 ### 8. `...
面试官主要问了基础知识、算法题、字符串判断包含判断、树的子树传输和小头传输、滑动窗口、epoll、select 模型、TCP 和 UDP、Linux top 和 ps 操作日志的一些指令、红黑树、SortSet、进程通信的方式等知识点。...
1. **字符串常量池**:当创建字符串时,如果该字符串已经在常量池中存在,则直接返回常量池中的引用。 2. **`new String()`与字符串字面量**:使用`new String("xyz")`创建的对象位于堆内存中,并且不会直接放入字符...