小站重新装修,欢迎到访。
标签 #Java

译文:@Contended和伪共享

原文: http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html

Java8引入了@Contented这个新的注解来减少伪共享(False Sharing)的发生。本文介绍了@Contented注解并解释了为什么False Sharing是如何影响性能的。

缓存行

CPU读取内存数据时并非一次只读一个字节,而是会读一段64字节长度的连续的内存块(chunks of memory),这些块我们称之为缓存行(Cache line)。

假设你有两个线程(Thread1和Thread2)都会修改同一个volatile变量x:

1
volatile long x;

如果Thread1先改变x的值,然后Thread2又去读它:

1
2
Thread 1: x=3;
Thread 2: System.out.print(x);

那么x所在缓存行上的所有64个字节的值都要被重新加载,因为CPU核心间交换数据是以缓存行为最小单位的。当然Thread1和Thread2是有可能在同一个核心上运行的,但我们此处假设两个线程在不同的核心上运行。

已知long类型占8个字节,缓存行长度为64个字节,那么一个缓存行可以保存8个long型变量,我们已经有了一个long型的x,假设x所在缓存行里还有其他7个long型变量,v1到v7:

1
x, v1, v2, v3, v4, v5 ,v6 ,v7

伪共享(False Sharing)

这个缓存行可以被许多线程访问。如果其中一个修改了v2,那么会导致Thread1和Thread2都会重新加载整个缓存行。你可能会疑惑为什么修改了v2会导致Thread1和Thread2重新加载该缓存行,毕竟只是修改了v2的值啊。虽然说这些修改逻辑上是互相独立的,但同一缓存行上的数据是统一维护的,一致性的粒度并非体现在单个元素上。这种不必要的数据共享就称之为“伪共享”(False Sharing)。

填充(Padding)

一个CPU核心在加载一个缓存行时要执行上百条指令。如果一个核心要等待另外一个核心来重新加载缓存行,那么他就必须等在那里,称之为stall(停止运转)。减少伪共享也就意味着减少了stall的发生,其中一个手段就是通过填充(Padding)数据的形式,来保证本应有可能位于同一个缓存行的两个变量,在被多线程访问时必定位于不同的缓存行。

在下面这个例子里,我们试图通过填充的方式,使得xv1位于不同的缓存行:

1
2
3
4
5
6
7
8
9
10
11
public class FalseSharingWithPadding {
public volatile long x;
public volatile long p2; // padding
public volatile long p3; // padding
public volatile long p4; // padding
public volatile long p5; // padding
public volatile long p6; // padding
public volatile long p7; // padding
public volatile long p8; // padding
public volatile long v1;
}

在你考虑使用填充之前,必须要了解的一点是JVM可能会清除无用字段或重排无用字段的位置,这样的话,可能无形中又会引入伪共享。我们也没有办法指定对象在堆内驻留的位置。

为了避免无用字段被消除,通常我们会用volatile修饰一下。个人建议只需为处于激烈竞争状态的类进行填充处理,而且一般只有通过对其性能分析才能发现性能上的不同。通常在性能分析时,最好在对其迭代访问10000次之后再去采样,这样可以消除JVM本身的运行时优化策略带来的影响。

Java8和@Contended

除了对字段进行填充之外,还有一个比较清爽的方法,那就是对需要避免陷入伪共享的字段进行注解,这个注解暗示JVM应当将字段放入不同的缓存行,这也正是JEP142的相关内容。

该JEP引入了@Contented注解。被这个注解修饰的字段应当和其他的字段驻留在不同的位置。

1
2
3
4
5
public class Point {
int x;
@Contended
int y;
}

上面的代码将x和y置于不同的缓存行。@Contented注解将y移动到远离对象头部的地方,(以避免和x一起被加载到同一个缓存行)。

参考

http://openjdk.java.net/projects/jdk8/features
http://beautynbits.blogspot.co.uk/2012/11/the-end-for-false-sharing-in-java.html
http://openjdk.java.net/jeps/142
http://mechanical-sympathy.blogspot.co.uk/2011/08/false-sharing-java-7.html
http://stackoverflow.com/questions/19892322/when-will-jvm-use-intrinsics
https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help

ketama的一个重要bug

ketama是什么?

C library for consistent hashing, and langauge bindings

至于consistent hasing(一致性哈希),不了解的可以参考这里

ketama是去年读这篇文章时看到的,应该是Richard Jones还在last.fm工作时写的,不过他现在已经离开了。今天莫名其妙地突然想起了它,所以特地又去搜了下,找到后顺便读了下源码。ketama最初是一个c库和一个PHP的封装,不过后来作者添加了多种其他语言的实现,还感慨Java集合类让实现简化了很多(实际上是用了SortedMap的API,我还读过一个常见的memcached java客户端,具体哪个忘了,也是用了SortedMap)。源码在github上可以看到。

问题就出在Java实现上。当我看到源码里声明了全局的java.security.MessageDigest时,意识到这里可能有并发问题。起初我还不太相信,于是去google了一把,网上似乎是说MessageDigest不是线程安全的,但我最后还是自己写了个简单的多线程程序测试以求最终确认,最后终于向自己证实确实是RJ同学出了bug。然后果断fork然后fix,最后向作者提交了patch,坐等处理。

我的patch很简单,将成员变量去掉,在函数中即时创建MessageDigest对象。为此我写了个简单的benchmark,发现MessageDigest.getInstance(String)方法在我的MBP MD313上一次调用耗时不足1ms,相对于网络开销来说基本可以忽略。

最后我去看了下apache-commons-codec库的org.apache.commons.codec.digest.DigestUtils类,发现也是类似的做法,遂放心。

类似的比较容易忽略的还有java.text.SimpleDateFormat,在编写Java类时,同学们一定要审慎对待每一个成员变量,以及其即将面临的运行环境。这其实很烦,这也是我喜欢FP的理由。

Java堆外内存泄露场景总结

去年在一次QA对我们的一台长连接服务器进行压测后,暴露了一个Java内存泄露问题,结合jconsole、jmap等一系列工具分析和代码review,最后发现是一个存放断开的Socket连接的容器,没有合适的清理释放逻辑。这是在java heap上发生的泄露,如果发生了non-heap上的内存泄露,一般会祭上google-perftools来分析。但不像堆上的泄露原因各种各样,non-heap的泄露原因比较常见的只有几种,因为日常开发中很少有机会操作non-heap内存。

这里总结一下java的non-heap内存泄露原因。其实说白了也就是介绍下日常开发中,哪里有可能接触到non-heap,哪些东西是放到non-heap上的。

####JNI

使用JNI有时为了提高效率,对于这种情况,如果效率提升不是非常大的话,我个人还是建议使用纯Java来替代JNI。正如我去年做的,兄弟部门把人脸识别的判决算法封装到JNI里,而我一向对自己写JNI不太放心,为了降低风险提高系统稳定性,干脆将核心算法翻译成Java了。但对于不得不使用JNI的场景,一定要注意内存的管理,但对于NewStringUTF()这种方法调用,则无需多虑,因为这其实是在java heap上创建Java对象。

####NIO

Java的nio有三种方法创建ByteBuffer:wrap(byte[])allocate(int)allocateDirect(int),第三种方法是在堆外申请内存,在使用较大块buffer的时候,能获得较高的效率。不过这种方式申请的内存可以被GC,但是如果使用不当导致泄漏,则比较难查找。

对于Oracle的HotSpot VM,可以用-XX:MaxDirectMemorySize指定direct buffer的最大值,这个默认值是0,即不限制。

说句题外话,Oracle的JDK7去掉了allocateDirect所分配内存的页对齐,这样可以减小分配很多小buffer的内存消耗。

####AWT/Swing
二者的一些bug会导致堆内存泄露,对于AWT/Swing引起的堆外内存泄露,我倒没什么经验,摘一个别人发的例子:

1
2
java.awt.Font font = java.awt.Font.createFont(Font.TRUETYPE_FONT, file);
font.deriveFont(Font.PLAIN, i)

至于哪些数据在堆外分配,读者可以阅读下源码分析下。对于这种情况,可以使用singleton模式解决。

####Inflater&Deflater

Java的zip包里两个重要类,记住一定要保证end()能被调用。这个可能比较常见一些,在此就不多做解释了。

1
2
3
4
5
6
7
8
9
10
11
Inflater inflater = null;
Deflater deflater = null;
try{
inflater = new Inflater();
deflater = new Deflater();
}catch(Exception e){
;
}finally{
inflater.end();
deflater.end();
}

以上就是四种比较常见的non-heap内存泄露原因,如果你的程序有堆外内存泄露的现象,可以着重从上述四个方面查找原因。

Ant的一个bug

前几日发现了ant的一个bug。我写的一个打包的ant脚本在我的mac上运行正常,但有同事在windows上则构建失败,我跑到我的windows上测试了下,居然也是失败的,定位了下问题,发现是<jar jarfile="path/to/a.jar"/>在windows上并未创建相应的path/to二级目录。这是我从没遇到过的情况,我首先想到的是去linux上试了下,打包正常这说明并非我的系统环境有多特殊,于是作为一个MacOS用户,一个诡异的想法便开始形成:我觉得这可能是ant在windows上的bug。

然后我去apache下载了最新的ant源码,Jar继承了Zip类,扫了一眼相关源码,看到一段:

1
2
3
4
File parent = zipFile.getParentFile();
if (parent != null && !parent.isDirectory() && !parent.mkdirs()) {
throw new BuildException("Failed to create missing parent" + " directory for " + zipFile);
}

我更疑惑了,看起来ant已经考虑到jar所在的目录的创建了啊。

于是我跑到windows里,准备调试一下相关逻辑,在eclipse里配置了调试环境,把我那个ant脚本作为参数,在开始debug前,我先运行了一下,居然成功了。越来越疑惑了。

仔细想了下,我决定把我windows上老的ANT_HOME切换到最新版的ant,然后事情开始明朗了,构建成功了。我终于恍然大悟,可能是因为一直忽略了ant的版本差异,导致浪费这么多精力。我用的是1.7.0版的ant构建出错,但查阅的却是1.9.2版的,难道新版已经修复了这个bug?为了证实这个猜测,我去ant的subversion仓库里,拉取了Zip.java近期的log,终于发现了REV:807960的log:

create parent directory of archive in and if needed. PR 45377. Based on patch by Remie Bolte

Ant patch

果不其然啊!可惜失去一个为开源社区贡献代码的机会。

[开源]DataX的oracle JDBC writer插件

项目地址:https://github.com/jingege/datax-oraclejdbcwriter

取之于开源,用之于开源

淘宝的DataX开源版本只提供了OCI方式的oracle writer plugin,部署起来有点麻烦。基于JDBC驱动的话,性能虽然不及OCI,但一般只要不是太苛刻的需求,还是能满足的。

DataX的插件机制让对其扩展变得十分简单,所以实现一个插件并非难事,而且笔者提交的代码也并不漂亮,故此次开源仅仅是作为对开源社区的一次微不足道的回馈,理所应当。

[开源]yaderde框架

项目地址:https://github.com/jingege/yaserde

其实当我决定写这个框架的时候,我突然意识到,我想要的正如hadoop的Writable做的,而且hadoop已经做得相当好了。可我还是决定要按自己的思路造一个轮子出来。一来自娱自乐,二来也算是对过去几年遗憾的稍许弥补。目前只做了个雏形,但对于简单场景还是可用的。

记一个Iterator的异常

今天在一个熟悉的场景首次遇到了异常,在使用ArrayList的Iterator时,抛出了java.lang.IllegalStateException。仔细分析后,发现是一个不合适的循环嵌套,导致了it.remove()连续调用了两次。结合源码看了下,AbstractList$Itr类使用了一个int lastRet变量标记最近一次next()所指向的元素,而在调用it.remove()时,先检查lastRet的值,如果为-1,则抛出IllegalStateException,否则将lastRet置为-1。用以保证一个元素只被remove一次。在Itr类的子类ListIterator里,previous()也是同样原理。

古人云:

吃一堑长一智

又云:

学如逆水行舟,不进则退

重点在吃和行两个字,讲究行动,一定要在实践中遇到困难,并解决,方能长一智,方能前进。对于码农,则一定是要多动手,多碰钉子,多独立解决问题,如是自勉。

对user.dir的误解

今天调试程序的时候,发现我之前hard code的一段路径修改成了System.getProperty("user.dir")之后,依然可以正常运行。我一直以来都以为user.dir就是当前用户的home dir,似乎这个印象应该自大学时代就已经有了,所以我有些先入为主地认为是环境有问题。

我的程序是最终由一个py脚本调用jar包来执行,为了搞明白这个困惑的问题,我在java代码里插入了多处out.println(System.getProperty("user.dir")),然后执行py,结果输出并非home dir。于是凭直觉将问题定位在py里的一句os.chdir(sys.path[0]+"/..")上。再然后我就写了个测试的Main函数,在控制台直接java -cp a.jar path.to.Main,果然输出了当前目录,于是问题清晰了。

于是网上查了下,竟然是我搞混了。如下摘自jdk文档

“user.dir” User working directory

“user.home” User home directory

最后我也终于记起来为什么会有这个错误的印象了。大学时用java写了个简单的http server,创建配置文件时,我默认写在了user.home,所以有了这个印象。如今看到user.dir,便和user.home混淆起来。

都写这么多年java了,让大家见笑了。问题也搞清楚了,我发现以后遇到路径问题,借助user.dir和脚本,可以非常方便的解决了。

简析Java Reference

Coding了多年Java,自然早就听说过四种引用类型,不过偶尔也才浅浅的用一下,今天突然想看看这块的内部实现,读了读jdk源码,这篇文章就当做个笔记。先简单介绍下四种引用类型:

####Java的四种引用类型####

  • StrongReference

强引用。最常见的引用类型,用赋值号,即“=”来创建。GC不会回收强引用,即使内存不足,也是宁可抛OOM先。

  • SoftReference

软引用。关键看内存,在内存不足时,GC会把软引用的对象回收掉。适用于对内存敏感的缓存。

  • WeakReference

弱引用。和软引用的区别是,无论内存充足与否,GC扫描到弱引用所引用的对象不再具有强引用时,会将其回收。

  • PhantomReference

虚引用,也称幽灵引用。其get()方法固定return null,看起来似乎没有什么大的用处,但在某些场景下结合ReferenceQueue,可以有意想不到的效果。

####ReferenceQueue####
站在coder的角度看,ReferenceQueue似乎更“有用”一些。其作用是追踪被gc的Reference对象,更多的是做一些统计或清理的工作。

####Reference的四种内部状态和ReferenceHandler####

Reference依赖内部的四种状态,和GC、ReferenceHandler配合来运作,包括:Active、Pending、Enqueued和Inactive,具体可以查看下Reference类的注释,鉴于笔者理解不足够深刻,就不误导大家了。

ReferenceHandler是个高优先级的线程,用于把gc处理的Reference对象enqueue到ReferenceQueue中。

近期使用SoftReference自己做了个缓存工具,用以缓存一定size的Hive查询结果,代码在这里

1

博主是一个不是很聪明的码农。完美主义者,强迫症中期。这里会记录一些回忆和点滴,以博为镜。

武器库:

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-SA 3.0  授权发布。最后生成于 2017-02-20.