Java中字符串去重的特性介绍_java

字符串在任何应用中都占用了大量的内存。尤其数包含独立UTF-16字符的char[]数组对JVM内存的消耗贡献最多——因为每个字符占用2位。

内存的30%被字符串消耗其实是很常见的,不仅是因为字符串是与我们互动的最好的格式,而且是由于流行的HTTP API使用了大量的字符串。使用Java 8 Update 20,我们现在可以接触到一个新特性,叫做字符串去重,该特性需要G1垃圾回收器,该垃圾回收器默认是被关闭的。

字符串去重利用了字符串内部实际是char数组,并且是final的特性,所以JVM可以任意的操纵他们。

对于字符串去重,开发者考虑了大量的策略,但最终的实现采用了下面的方式:

无论何时垃圾回收器访问了String对象,它会对char数组进行一个标记。它获取char数组的hash value并把它和一个对数组的弱引用存在一起。只要垃圾回收器发现另一个字符串,而这个字符串和char数组具有相同的hash code,那么就会对两者进行一个字符一个字符的比对。

如果他们恰好匹配,那么一个字符串就会被修改,指向第二个字符串的char数组。第一个char数组就不再被引用,也就可以被回收了。

这整个过程当然带来了一些开销,但是被很紧实的上限控制了。例如,如果一个字符未发现有重复,那么一段时间之内,它会不再被检查。

那么该特性实际上是怎么工作的呢?首先,你需要刚刚发布的Java 8 Update 20,然后按照这个配置: -Xmx256m -XX:+UseG1GC 去运行下列的代码:
 

public class LotsOfStrings {

 private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>();

 public static void main(String[] args) throws Exception {
  int iteration = 0;
  while (true) {
   for (int i = 0; i < 100; i++) {
    for (int j = 0; j < 1000; j++) {
     LOTS_OF_STRINGS.add(new String("String " + j));
    }
   }
   iteration++;
   System.out.println("Survived Iteration: " + iteration);
   Thread.sleep(100);
  }
 }
}

这段代码会执行30个迭代之后报OutOfMemoryError。

现在,开启字符串去重,使用如下配置去跑上述代码:

-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics

此时它已经可以运行更长的时间,而且在50个迭代之后才终止。

JVM现在同样打印出了它做了什么,让我们一起看一下:

 

[GC concurrent-string-deduplication, 4658.2K->0.0B(4658.2K), avg 99.6%, 0.0165023 secs]
  [Last Exec: 0.0165023 secs, Idle: 0.0953764 secs, Blocked: 0/0.0000000 secs]
   [Inspected:     119538]
     [Skipped:       0( 0.0%)]
     [Hashed:     119538(100.0%)]
     [Known:        0( 0.0%)]
     [New:       119538(100.0%)  4658.2K]
   [Deduplicated:    119538(100.0%)  4658.2K(100.0%)]
     [Young:       372( 0.3%)   14.5K( 0.3%)]
     [Old:       119166( 99.7%)  4643.8K( 99.7%)]
  [Total Exec: 4/0.0802259 secs, Idle: 4/0.6491928 secs, Blocked: 0/0.0000000 secs]
   [Inspected:     557503]
     [Skipped:       0( 0.0%)]
     [Hashed:     556191( 99.8%)]
     [Known:       903( 0.2%)]
     [New:       556600( 99.8%)   21.2M]
   [Deduplicated:    554727( 99.7%)   21.1M( 99.6%)]
     [Young:       1101( 0.2%)   43.0K( 0.2%)]
     [Old:       553626( 99.8%)   21.1M( 99.8%)]
  [Table]
   [Memory Usage: 81.1K]
   [Size: 2048, Min: 1024, Max: 16777216]
   [Entries: 2776, Load: 135.5%, Cached: 0, Added: 2776, Removed: 0]
   [Resize Count: 1, Shrink Threshold: 1365(66.7%), Grow Threshold: 4096(200.0%)]
   [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0]
   [Age Threshold: 3]
  [Queue]
   [Dropped: 0]

为了方便,我们不需要自己去计算所有数据的加和,使用方便的总计就可以了。

上面的代码段规定执行了字符串去重,花了16ms的时间,查看了约 120 k 字符串。

上面的特性是刚推出的,意味着可能并没有被全面的审视。具体的数据在实际的应用中可能看起来有差别,尤其是那些应用中字符串被多次使用和传递,因此一些字符串可能被跳过或者早就有了hashcode(正如你可能知道的那样,一个String的hash code是被懒加载的)。

在上述的案例中,所有的字符串都被去重了,在内存中移除了4.5MB的数据。

[Table]部分给出了有关内部跟踪表的统计信息,[Queue]则列出了有多少对去重的请求由于负载被丢弃,这也是开销减少机制中的一部分。

那么,字符串去重和字符串驻留相比又有什么差别呢?事实上,字符串去重和驻留看起来差不多,除了暂留的机制重用了整个字符串实例,而不仅仅是字符数组。

JDK Enhancement Proposal 192的创造者的争论点在于开发者们常常不知道将驻留字符串放在哪里合适,或者是合适的地方被框架所隐藏.就像我写的那样,当碰到复制字符串(像国家名字)的时候,你需要一些常识.字符串去重,对于在同一个JVM中的应用程序的字符串复制也有好处,同样包括像XML Schemas,urls以及jar名字等一般认为不会出现多次的字符串.

当字符串驻留发生在应用程序线程中的时候,垃圾回收异步并发处理时,字符串去重也不会增加运行时的消耗.这也解释了,为什么我们会在上面的代码中发现Thread.sleep().如果没有sleep会给GC增加太多的压力,这样字符串去重根本就不会发生.但是,这只是示例代码才会出现的问题.实际的应用程序,常常会在运行字符串去重的时候使用几毫秒的时间.

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索java
字符串去重
java8新特性、java9新特性、java 1.8 新特性、java高级特性、java7新特性,以便于您获取更多的相关知识。

时间: 2024-12-01 06:39:34

Java中字符串去重的特性介绍_java的相关文章

基于Java中字符串内存位置详解_java

前言 之前写过一篇关于JVM内存区域划分的文章,但是昨天接到蚂蚁金服的面试,问到JVM相关的内容,解释一下JVM的内存区域划分,这部分答得还不错,但是后来又问了Java里面String存放的位置,之前只记得String是一个不变的量,应该是要存放在常量池里面的,但是后来问到new一个String出来应该是放到哪里的,这个应该是放到堆里面的,后来又问到String的引用是放在什么地方的,当时傻逼的说也是放在堆里面的,现在总结一下:基本类型的变量数据和对象的引用都是放在栈里面的,对象本身放在堆里面,

java中的自增问题介绍_java

运行下面这段代码,其结果是什么呢? package com.test; public class Inc { public static void main(String[] args) { Inc inc = new Inc(); int i = 0; inc.fermin(i); i = i++; System.out.println(i); } void fermin(int i) { i++; } } 结果是:0 上述类似代码在C/C++中运行结果是:1,这是为什么呢?   这是因为Ja

Java中的深拷贝和浅拷贝介绍_java

一.引言   对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去.在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部 数据.Java中有三种类型的对象拷贝:浅拷贝(Shallow Copy).深拷贝(Deep Copy).延迟拷贝(Lazy Copy). 二.浅拷贝 1.什么是浅拷贝   浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝.如果属性是基本类型,拷贝的就是基本类型的值:如果属性是内存地

Java中的阻塞队列详细介绍_java

Java中的阻塞队列 1. 什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列.这两个附加的操作是: 在队列为空时,获取元素的线程会等待队列变为非空. 当队列满时,存储元素的线程会等待队列可用. 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程.阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素. 2.Java里的阻塞队列 JDK中提供了七个阻塞队列: ArrayBlockingQueue :一个由数组结

java 中函数的参数传递详细介绍_java

 java中函数的参数传递 总结:   1.将对象(对象的引用)作为参数传递时传递的是引用(相当于指针).也就是说函数内对参数所做的修改会影响原来的对象.   2.当将基本类型或基本类型的包装集作为参数传递时,传递的是值.也就是说函数内对参数所做的修改不会影响原来的变量.   3.数组(数组引用))作为参数传递时传递的是引用(相当于指针).也就是说函数内对参数所做的修改会影响原来的数组.   4.String类型(引用)作为参数传递时传递的是引用,只是对String做出任何修改时有一个新的Str

Java中的Object类详细介绍_java

理论上Object类是所有类的父类,即直接或间接的继承java.lang.Object类.由于所有的类都继承在Object类,因此省略了extends Object关键字. 该类中主要有以下方法: toString(),getClass(),equals(),clone(),finalize(), 其中toString(),getClass(),equals是其中最重要的方法. 注意: Object类中的getClass(),notify(),notifyAll(),wait()等方法被定义为f

java中的枚举类型详细介绍_java

枚举中有values方法用于按照枚举定义的顺序生成一个数组,可以用来历遍.我们自定义的枚举类都是继承自java.lang.Enum,拥有一下实例中的功能: 复制代码 代码如下: //: enumerated/EnumClass.java // Capabilities of the Enum class import static net.mindview.util.Print.*; enum Shrubbery { GROUND, CRAWLING, HANGING } public clas

JAVA中 终止线程的方法介绍_java

在Java的多线程编程中,java.lang.Thread类型包含了一些列的方法start(), stop(), stop(Throwable) and suspend(), destroy() and resume().通过这些方法,我们可以对线程进行方便的操作,但是这些方法中,只有start()方法得到了保留. 在Sun公司的一篇文章<Why are Thread.stop, Thread.suspend and Thread.resume Deprecated? >中详细讲解了舍弃这些方

Java中的final关键字详细介绍_java

•final变量如果在变量前加final关键字,则这个变量一旦被初始化,便不可再改变. 如果一个final变量是类成员变量,则必须被初始化,且只能被初始化一次. 方法中的参数也可以是final变量.这在我们需要传递引用型的变量时非常有用,因为有时候我们并不希望调用函数修改该变量而影响到原函数中对象的值.因此将引用型变量设为final类型可以有效方式变量被调用参数修改.此时在调用方法中只可以使用该变量,但不能对其做任何修改. 复制代码 代码如下: void test(final int a){