上坡的路都是累的设置难受的,要控制好自己的情绪。
–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning
目录
前言
继上篇文章分享了Eureka内置的一个小工具RateLimiter限流器后,本文继续分享其另外一个实用小工具:StringCache
。这两工具均是与业务代码无关(也就是和Eureka本身无关),仅依赖于JDK,因此具有最好的移植性(代码复制一下就可以开干),可以任意地方随意使用。
关于字符串缓存,一直就是个热门以及高性能下必须考虑的问题,如String#intern()
的使用可不能太过于随意,本文会一并尝试讨论。
正文
StringCache
是Eureka 自己实现的对字符串的缓存,在这之前我们先来认识认识String#intern()
。
详解String#intern()
在 JAVA 语言中有8种基本类型和一种比较特殊的类型String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。
8种基本类型(byte、short、int、long、float、double、boolean、char
)的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
String#intern()
源码:
String:
// 它是个native方法 -> 用c/c++实现
public native String intern();
这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
小总结一下,String#intern()
方法能有如下特点:
- 字符串常量池能带来速度更快,更节省内存的好处(不用一个字符串存储多份了嘛)
- 非双引号声明的 String 对象,需要使用
String#intern()
方法,将字符串存储到字符串常量池
既然已经有这个系统级的缓存可用了,那为何Eureka还要多此一举,自己搞个StringCache
呢?这就需要继续深入String#intern()
,了解它的不足了。
源码(native代码)参阅
因为它的源码是c编写,因此以下部分引用自“美团技术团队”,欢迎关注其官方公众号,内容非常的干。
... // 假设有源码
细节说明:在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发人员声明 openJdk7 和 jdk7 使用的是同一分主代码,只是分支代码会有些许的变动。所以可以直接跟踪 openJdk7 的源码来探究 intern 的实现。
它的大体实现结构就是:JAVA 使用 jni 调用c++实现的StringTable
(说明:此类理应是每个Java Coder多少应该了解一些的哦)的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的,只是不能自动扩容,默认大小是1009。
到这:似乎已经找到了
StringTable
/String#intern()
的不足之处了吧~
要注意的是,String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:
-XX:StringTableSize=99991
代码示例
相信每个人都做过这样一道面试题:String s = new String("abc")
这个语句创建了几个对象?这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。
@Test
public void fun1() {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
请注意这个结果在JDK6和JDK7中的结果是不一样的。其实关于intern()
这部分代码最初来自于美团点评,后来天下被各种抄袭,此处我不重复了,请您参考美团点评的原著:深入解析String#intern
这里我只提醒各位自己做试验的时候,在切换JDK时候的注意事项:
如果你项目应用的JDK是1.8,但你只是在这调整了语言级别,那么你可能是看不到效果的,因为它只管编译,运行实际还是JDK8。正确做法应该是:
只有这么做,才能确保结果是你想要的。
Eureka中的StringCache
知晓了String#intern()
的StringTable不能动态扩容的局限性之后,不难理解Eureka为何要搞个StringCache
了吧。官方Javadoc解释:用于替代String#intern()
,它没有容量限制(with no capacity constraints)。
StringCache
顾名思义它表是字符串缓存,对于那些并不需要动态性的字符串值,就可以使用该缓存,以避免每次都新开辟空间而浪费内存。比如eureka里的appName、appGroupName、vipAddress、secureVipAddress
等等都会使用它来缓存。
public class StringCache {
// 缓存的字符串的最大长度:超过38个我就不缓存你了
public static final int LENGTH_LIMIT = 38;
// 懒汉式
private static final StringCache INSTANCE = new StringCache();
// 读写锁
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// 底层使用的WeakHashMap做的缓存,value是个WeakReference若引用
private final Map<String, WeakReference<String>> cache = new WeakHashMap<String, WeakReference<String>>();
// 默认值是LENGTH_LIMIT
private final int lengthLimit;
// 虽然提供了一个INSTANCE实例,但是它的构造器仍旧是公开的
public StringCache() {
this(LENGTH_LIMIT);
}
public StringCache(int lengthLimit) {
this.lengthLimit = lengthLimit;
}
}
该字符串缓存底层使用的是WeakHashMap
作为存储,因此它可以动态扩容,是没有存储限制的。
使用方法
它既提供了一个static的INSTANCE
实例,并且构造器却又都是public的,因此可想而知它是支持两种使用方式的,但底层归口是同一个。
StringCache:
public String cachedValueOf(final String str) {
// 若字符串超过长度了(默认38),就不予以缓存了
// 当然若你设置lengthLimit < 0的值,代表缓存所有的串
// 注意:"" " "这种空串也都是会缓存的哦
if (str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
try {
// 上读锁,看看缓存里是否有当前字符串,有就直接return
lock.readLock().lock();
WeakReference<String> ref = cache.get(str);
if (ref != null) {
return ref.get();
}
} finally {
lock.readLock().unlock();
}
// 若缓存里没有,就写进缓存里去
// 注意:每个value都是一个new WeakReference<>(str)
// 弱引用:当gc时,无论内存是否充足,都会回收被弱引用关联的对象
try {
lock.writeLock().lock();
WeakReference<String> ref = cache.get(str);
if (ref != null) {
return ref.get();
}
cache.put(str, new WeakReference<>(str));
} finally {
lock.writeLock().unlock();
}
return str;
}
return str;
}
// 缓存大小
public int size() {
try {
lock.readLock().lock();
return cache.size();
} finally {
lock.readLock().unlock();
}
}
逻辑非常的简单,优点是利用了弱引用,这样只要GC发生时,无论内存是否充足都会回收,对内存友好。
另外StringCache
还提供了一个静态方法,方便使用(大部分情况均是使用此静态方法完成缓存工作):
StringCache:
public static String intern(String original) {
return INSTANCE.cachedValueOf(original);
}
代码示例
@Test
public void fun1() {
String str1 = new String("YoutBatman");
String str2 = new String("YoutBatman");
System.out.println(str1 == str2);
StringCache cache = new StringCache();
String cachedStr1 = cache.cachedValueOf(str1);
String cachedStr2 = cache.cachedValueOf(str2);
System.out.println(cachedStr1 == cachedStr2);
}
运行程序,控制台输出:
false
true
在实际使用过程中,对于不怎么动态变化的我们一般这么赋值:
@Test
public void fun2() {
String appName = StringCache.intern("YoutBatman");
}
对比Hystrix里的InternMap缓存
不仅是Eureka里,在Hystrix里也是有一个类似的用户缓存字符串的类的,它叫InternMap
:
InternMap:
private final ConcurrentMap<K, V> storage = new ConcurrentHashMap<K, V>();
...
public V interned(K key) {
V existingKey = storage.get(key);
V newKey = null;
if (existingKey == null) {
newKey = valueConstructor.create(key);
existingKey = storage.putIfAbsent(key, newKey);
}
return existingKey != null ? existingKey : newKey;
}
public int size() {
return storage.size();
}
它直接使用ConcurrentMap
进行缓存来使用,相对来说并没有StringCache
那样使用WeakHashMap
+ 读写锁ReadWriteLock
的方式来得那么高效和对内存友好,属于低配版。
总结
关于Eureka内置公用小工具:StringCache及详解String#intern()就先介绍这,本文其实是有难点的,那边是对String#intern()
的深入理解,建议有兴趣者看美团点评的那篇技术博客,并且一定一定要自己敲代码做试验,否则你一定扭头就忘。
使用建议:建议使用StringCache
代替String#intern()
和InternMap
(提示:若你工程没有引入Eureka Client做依赖,你大可以把这个类拷贝到你项目内来使用它)。
声明
原创不易,码字更不易,感谢关注。分享本文到你的朋友圈是被授权的,但拒绝抄袭。【左边扫码加我wx / wx号:fsx641385712
】,邀你加入 【Java高工、架构师】 系列纯纯纯技术群,亦可扫码加入我的知识星球【BAT的乌托邦】。
- 3分钟带你了解轻量级依赖注入框架Google Guice【享学Java】
- [享学Eureka] 一、源生Eureka介绍 — 基于注册中心的服务发现
- [享学Eureka] 二、Eureka的最核心概念:InstanceInfo实例信息
- [享学Eureka] 三、Eureka配置之:EurekaInstanceConfig实例配置
- [享学Eureka] 四、Eureka配置之:EurekaClientConfig客户端配置
- [享学Eureka] 五、Eureka核心概念:应用(Application)和注册表(Applications)
- [享学Eureka] 六、InstanceInfo实例管理器:ApplicationInfoManager
- [享学Eureka] 七、远程通信模块:EurekaHttpClient接口抽象以及基于Jersey的Low-Level实现JerseyApplicationClient
- [享学Eureka] 八、远程通信模块:手动构建JerseyApplicationClient客户端完成服务注册、服务下线…
- [享学Eureka] 九、远程通信模块:使用TransportClientFactory构建底层请求客户端完成服务注册、服务下线
- [享学Eureka] 十、迷人小工具之TimedSupervisorTask:自动调节执行间隔的周期性任务
- [享学Eureka] 十一、迷人小工具之EndpointUtils:从配置文件中解析出serviceUrl(非常重要)
- [享学Eureka] 十二、远程通信模块:集群解析器ClusterResolver(一) ConfigClusterResolver
- [享学Eureka] 十三、集群解析器ClusterResolver(二):ApplicationsResolver和EurekaHttpResolver
- [享学Eureka] 十四、集群解析器ClusterResolver(三):ZoneAffinityClusterResolver区域感知解析器
- [享学Eureka] 十五、集群解析器ClusterResolver(四):AsyncResolver异步解析器
- [享学Eureka] 十六、远程通信模块:Top Level部分之EurekaHttpClientFactory和SessionedEurekaHttpClient
- [享学Eureka] 十七、远程通信模块:RetryableEurekaHttpClient高可用Client端的重试机制
- [享学Eureka] 十八、远程通信模块:结合代码示例详解transport.retryableClientQuarantineRefreshPercentage配置项
- [享学Eureka] 十九、远程通信模块:EurekaHttpClients工具快速构建ClusterResolver集群解析器
- [享学Eureka] 二十、远程通信模块:EurekaHttpClients工具快速构建EurekaHttpClient请求客户端
- [享学Eureka] 二十一、LookupService服务发现之客户端实现:EurekaClient接口
- [享学Eureka] 二十二、DiscoveryClient服务注册的小工具:InstanceInfoReplicator
- [享学Eureka] 二十三、DiscoveryClient前置知识:BackupRegistry备用注册中心、HealthCheckHandler健康检查处理器…
- [享学Eureka] 二十四、DiscoveryClient透彻解析(一):功能概述 + 成员属性详解
- [享学Eureka] 二十五、DiscoveryClient透彻解析(二):初始化逻辑详解
- [享学Eureka] 二十六、DiscoveryClient透彻解析(三):全量获取注册表
- [享学Eureka] 二十七、DiscoveryClient透彻解析(四):增量获取注册表
- [享学Eureka] 二十八、DiscoveryClient透彻解析(五):HeartbeatThread和CacheRefreshThread
- [享学Eureka] 二十九、DiscoveryClient透彻解析(六):fetchRegistry() 拉取注册表信息
- [享学Eureka] 三十、DiscoveryClient透彻解析(七):initScheduledTasks() 初始化调度任务
- [享学Eureka] 三十一、DiscoveryClient透彻解析(八):接口方法和shutdown()方法详解
- [享学Eureka] 三十二、Eureka内置公用小工具:基于令牌桶算法的RateLimiter限流器