from:http://blog.csdn.net/tanxiang21/article/details/16859781
1.语法糖 数字下划线

 1  2  3  4  5  6  7  8  9
package com.java7developer.chapter1;
import java.util.Collection;
import java.util.HashMap;
public class Coin {
int test = 123_567;
long test1 = 100_000L;
}
 来自CODE的代码片
Coin.java

2.switch语句中的String

 1  2  3  4
public void printDay(String dayOfWeek){
case "Sunday":System.out.println("ddd");break;
default:System.out.println("sss");break;
}
 来自CODE的代码片
snippet_file_0.txt

3.multicatch

  1   2   3   4   5   6   7   8   9  10  11  12  13  14
public Configuration getConfig(String fileName) {
Configuration cfg = null;
try {
String fileText = getFile(fileName);
cfg = verifyConfig(parseConfig(fileText));
} catch (FileNotFoundException | ParseException | ConfigurationException e) {
System.err.println("Config file '" + fileName
+ "' is missing or malformed");
} catch (IOException iox) {
System.err.println("Error while processing file '" + fileName + "'");
}
return cfg;
}
 来自CODE的代码片
snippet_file_0.txt

4.final重抛

对比上份代码
 1  2  3  4  5  6
try {
String fileText = getFile(fileName);
cfg = verifyConfig(parseConfig(fileText));
} catch (final Exception e) {
throw e;
}
 来自CODE的代码片
snippet_file_0.txt

5.try-with-resources(TWR) AutoCloseable

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36
package com.java7developer.chapter1;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
public class Java7ResourcesExample {
private void run() throws IOException {
File file = new File("foo");
URL url = null;
try {
url = new URL("http://www.google.com/");
} catch (MalformedURLException e) {
}
try (OutputStream out = new FileOutputStream(file);
InputStream is = url.openStream()) {
byte[] buf = new byte[4096];
int len;
while ((len = is.read(buf)) > 0) {
out.write(buf, 0, len);
}
}
}
public static void main(String[] args) throws IOException {
Java7ResourcesExample instance = new Java7ResourcesExample();
instance.run();
}
}
 来自CODE的代码片
Java7ResourcesExample.java

6.钻石语法

 1
HashMap<String, String> a = new HashMap<>();
 来自CODE的代码片
Java7-新特性-钻石语法

7.变参 消失的警告 @SafeVarargs

  1   2   3   4   5   6   7   8   9  10  11  12  13  14  15
public class Coin {
int test = 123_567;
long test1 = 100_000L;
@SafeVarargs
public static <T> Collection<T> doSomething(T... entries){
return null;
}
public static void main(String[] args) {
HashMap<String, String> a = new HashMap<>();
HashMap<String, String> b = new HashMap<>();
doSomething(a,b);
}
}
posted @ 2015-03-06 16:35 小马歌 阅读(236) | 评论 (0)编辑 收藏
 

from:http://www.lifebackup.cn/timsort-java7.html

2012年09月25日 21:57:48

1. 为什么写这篇文章

这篇文章的根源是在产品中发现了一个诡异的bug:只能在产品环境下重现,在我的本地开发环境无法重现,而双方的代码没有任何区别。最后用remote debug的方法找到异常所在:

Exception in thread "main" java.lang.IllegalArgumentException: Comparison 
method violates its general contract!

Google了这个错误,是由于Java 7内置的新排序算法导致的。这才猛然想起产品的编译环境最近升级到了Java 7。

2. 结论

在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,内部实现换成了TimSort,其对对象间比较的实现要求更加严格:

Comparator的实现必须保证以下几点(出自这儿):

a). sgn(compare(x, y)) == -sgn(compare(y, x)) 
b). (compare(x, y)>0) && (compare(y, z)>0) 意味着 compare(x, z)>0 
c). compare(x, y)==0 意味着对于任意的z:sgn(compare(x, z))==sgn(compare(y, z)) 均成立

而我们的代码中,某个compare()实现片段是这样的:

public int compare(ComparatorTest o1, ComparatorTest o2) { 
    return o1.getValue() > o2.getValue() ? 1 : -1; 
}

这就违背了a)原则:假设X的value为1,Y的value也为1;那么compare(X, Y) ≠ –compare(Y, X) 
PS: TimSort不仅内置在各种JDK 7的版本,也存在于Android SDK中(尽管其并没有使用JDK 7)。

3. 解决方案

3.1) 更改内部实现:例如对于上个例子,就需要更改为

public int compare(ComparatorTest o1, ComparatorTest o2) { 
    return o1.getValue() == o2.getValue() ? 0 :  
                (o1.getValue() > o2.getValue() ? 1 : -1); 
}

3.2) Java 7预留了一个接口以便于用户继续使用Java 6的排序算法:在启动参数中(例如eclipse.ini)添加-Djava.util.Arrays.useLegacyMergeSort=true

3.3) 将这个IllegalArgumentException手动捕获住(不推荐)

4. TimSort在Java 7中的实现

那么为什么Java 7会将TimSort作为排序的默认实现,甚至在某种程度上牺牲它的兼容性(在stackoverflow上有大量的问题是关于这个新异常的)呢?接下来我们不妨来看一看它的实现。

首先建议大家先读一下这篇文章以简要理解TimSort的思想。

4.1) 如果传入的Comparator为空,则使用ComparableTimSort的sort实现。

 image

4.2) 传入的待排序数组若小于MIN_MERGE(Java实现中为32,Python实现中为64),则

a) 从数组开始处找到一组连接升序或严格降序(找到后翻转)的数 
b) Binary Sort:使用二分查找的方法将后续的数插入之前的已排序数组

image

4.3) 开始真正的TimSort过程:

4.3.1) 选取minRun大小,之后待排序数组将被分成以minRun大小为区块的一块块子数组

a) 如果数组大小为2的N次幂,则返回16(MIN_MERGE / 2) 
b) 其他情况下,逐位向右位移(即除以2),直到找到介于16和32间的一个数

image

4.3.2) 类似于4.2.a找到初始的一组升序数列 
4.3.3) 若这组区块大小小于minRun,则将后续的数补足(采用binary sort插入这个数组) 
4.3.4) 为后续merge各区块作准备:记录当前已排序的各区块的大小 
4.3.5) 对当前的各区块进行merge,merge会满足以下原则(假设X,Y,Z为相邻的三个区块):

a) 只对相邻的区块merge 
b) 若当前区块数仅为2,If X<=Y,将X和Y merge 
b) 若当前区块数>=3,If X<=Y+Z,将X和Y merge,直到同时满足X>Y+Z和Y>Z

image

4.3.6) 重复4.3.2 ~ 4.3.5,直到将待排序数组排序完 
4.3.7) Final Merge:如果此时还有区块未merge,则合并它们

image

5. Demo

这一节用一个具体的例子来演示整个算法的演进过程:

*注意*:为了演示方便,我将TimSort中的minRun直接设置为2,否则我不能用很小的数组演示。。。同时把MIN_MERGE也改成2(默认为32),这样避免直接进入binary sort。

初始数组为[7,5,1,2,6,8,10,12,4,3,9,11,13,15,16,14] 
=> 寻找连续的降序或升序序列 (4.3.2) 
[1,5,7] [2,6,8,10,12,4,3,9,11,13,15,16,14] 
=> 入栈 (4.3.4) 
当前的栈区块为[3] 
=> 进入merge循环 (4.3.5) 
do not merge因为栈大小仅为1 
=> 寻找连续的降序或升序序列 (4.3.2) 
[1,5,7] [2,6,8,10,12] [4,3,9,11,13,15,16,14] 
=> 入栈 (4.3.4) 
当前的栈区块为[3, 5] 
=> 进入merge循环 (4.3.5) 
merge因为runLen[0]<=runLen[1] 
1) gallopRight:寻找run1的第一个元素应当插入run0中哪个位置(”2”应当插入”1”之后),然后就可以忽略之前run0的元素(都比run1的第一个元素小) 
2) gallopLeft:寻找run0的最后一个元素应当插入run1中哪个位置(”7”应当插入”8”之前),然后就可以忽略之后run1的元素(都比run0的最后一个元素大) 
这样需要排序的元素就仅剩下[5,7] [2,6],然后进行mergeLow 
完成之后的结果: 
[1,2,5,6,7,8,10,12] [4,3,9,11,13,15,16,14] 
=> 入栈 (4.3.4) 
当前的栈区块为[8] 
退出当前merge循环因为栈中的区块仅为1 
=> 寻找连续的降序或升序序列 (4.3.2) 
[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16,14] 
=> 入栈 (4.3.4) 
当前的栈区块大小为[8,2] 
=> 进入merge循环 (4.3.5) 
do not merge因为runLen[0]>runLen[1] 
=> 寻找连续的降序或升序序列 (4.3.2) 
[1,2,5,6,7,8,10,12] [3,4] [9,11,13,15,16] [14] 
=> 入栈 (4.3.4) 
当前的栈区块为[8,2,5] 
=> 
do not merege run1与run2因为不满足runLen[0]<=runLen[1]+runLen[2] 
merge run2与run3因为runLen[1]<=runLen[2] 
1) gallopRight:发现run1和run2就已经排好序 
完成之后的结果: 
[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14] 
=> 入栈 (4.3.4) 
当前入栈的区块大小为[8,7] 
退出merge循环因为runLen[0]>runLen[1] 
=> 寻找连续的降序或升序序列 (4.3.2) 
最后只剩下[14]这个元素:[1,2,5,6,7,8,10,12] [3,4,9,11,13,15,16] [14] 
=> 入栈 (4.3.4) 
当前入栈的区块大小为[8,7,1] 
=> 进入merge循环 (4.3.5) 
merge因为runLen[0]<=runLen[1]+runLen[2] 
因为runLen[0]>runLen[2],所以将run1和run2先合并。(否则将run0和run1先合并) 
1) gallopRight & 2) gallopLeft 
这样需要排序的元素剩下[13,15] [14],然后进行mergeHigh 
完成之后的结果: 
[1,2,5,6,7,8,10,12] [3,4,9,11,13,14,15,16] 当前入栈的区块为[8,8] 
=> 
继续merge因为runLen[0]<=runLen[1] 
1) gallopRight & 2) gallopLeft 
需要排序的元素剩下[5,6,7,8,10,12] [3,4,9,11],然后进行mergeHigh 
完成之后的结果: 
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] 当前入栈的区块大小为[16] 
=> 
不需要final merge因为当前栈大小为1 
=> 
结束

6. 如何重现文章开始提到的Exception

这一节将剥离复杂的业务逻辑,用一个最简单的例子(不修改TimSort.java内置的各种参数)重现文章开始提到的Exception。因为尽管google出来的结果中非常多的人提到了这个Exception及解决方案,但并没有人给出一个可以重现的例子和测试数据。另一方面,我也想从其他角度来加深对这个问题的理解。

构造测试数据的过程是个反人类的过程:( 大家不要学我。。

以下是能重现这个问题的代码:

public class ReproJava7Exception { 
    public static void main(String[] args) { 
        int[] sample = new int[] 
              {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 
                0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-2,1,0,-2,0,0,0,0}; 
        List<Integer> list = new ArrayList<Integer>(); 
        for (int i : sample) 
            list.add(i); 
        // use the native TimSort in JDK 7 
        Collections.sort(list, new Comparator<Integer>() { 
            @Override 
            public int compare(Integer o1, Integer o2) { 
                // miss the o1 = o2 case on purpose 
                return o1 > o2 ? 1 : -1; 
            } 
        }); 
    } 
}

7. Sample Code

这篇文章的所有代码可以到github:https://github.com/Huang-Wei/understanding-timsort-java7下载。

8. References

http://en.wikipedia.org/wiki/Timsort 
http://www.geneffects.com/briarskin/theory/binary/index.html 
http://docs.oracle.com/javase/6/docs/api/java/util/Comparator.html#compare%28T,%20T%29 
http://www.oracle.com/technetwork/java/javase/compatibility-417013.html#source

分类:01呓语 | 标签:  |

9条评论

  1. hute说道:

    太好了,找了一天.感谢.

  2. hute说道:

    重现bug的方法确实很逆天,作者花了很大力气吧.

  3. superpippo说道:

    @hute 所以说这个过程是很反人类的。。。

  4. cjnetwork说道:

    你好,我测试了一下你提供的测试代码,发现还是不能复现这个异常情况,能否帮忙一下呢。

    以下是我的jdk:
    java version “1.7.0-ea”
    Java(TM) SE Runtime Environment (build 1.7.0-ea-b45)
    Java HotSpot(TM) Client VM (build 14.0-b10, mixed mode, sharing)

  5. superpippo说道:

    @cjnetwork 我在jdk 1.7.0_17上用ReproJava7Exception.java能重现这个问题
    我猜测你不能重现有两种可能:
    1) 你的这个jdk版本有点诡异(1.7.0-ea),尝试升到最1.7的正式版本
    2) 确认你在编译时候使用的是JDK7,而不是JDK6 – 因为有可能在Eclipse或别的IDE中并没有设置正确。你可以写一段switch string的小例子看看有没有编译错误

  6. ghsau说道:

    compare实现只需要这样:
    return o1.getValue() – o2.getValue();
    不需要自己判断.

posted @ 2015-02-26 18:46 小马歌 阅读(276) | 评论 (0)编辑 收藏
 
     摘要: Python2.3中开始使用的timsort应该说算是声名在外了,不管是在稳定性还是在速度上都十分的惊人。前一段刚刚看了《Python CookBook》中的一些章节,对timsort产生了一些兴趣。于是在网上看到了这边文章,讲的相当清楚明了,于是产生了翻译的念头,也于是有了这篇文章。这应该算是我翻译的第一篇技术文章,真正做一次才明白能看懂和能翻译出来还是有蛮大的差距的。翻译质量不可谓不差,诸位如...  阅读全文
posted @ 2015-02-26 18:30 小马歌 阅读(412) | 评论 (0)编辑 收藏
 
     摘要: 概要这个类在 Oracle 的官方文档里是查不到的,但是确实在 OpenJDK 的源代码里出现了,Arrays 中的 sort 函数用到了这个用于排序的类。它将归并排序(merge sort) 与插入排序(insertion sort) 结合,并进行了一些优化。对于已经部分排序的数组,时间复杂度远低于 O(n log(n)),最好可达&n...  阅读全文
posted @ 2015-02-26 15:59 小马歌 阅读(488) | 评论 (0)编辑 收藏
 
from:http://www.infoq.com/cn/articles/netty-version-upgrade-history-thread-part

1. 背景

1.1. Netty 3.X系列版本现状

根据对Netty社区部分用户的调查,结合Netty在其它开源项目中的使用情况,我们可以看出目前Netty商用的主流版本集中在3.X和4.X上,其中以Netty 3.X系列版本使用最为广泛。

Netty社区非常活跃,3.X系列版本从2011年2月7日发布的netty-3.2.4 Final版本到2014年12月17日发布的netty-3.10.0 Final版本,版本跨度达3年多,期间共推出了61个Final版本。

1.2. 升级还是坚守老版本

相比于其它开源项目,Netty用户的版本升级之路更加艰辛,最根本的原因就是Netty 4对Netty 3没有做到很好的前向兼容。

由于版本不兼容,大多数老版本使用者的想法就是既然升级这么麻烦,我暂时又不需要使用到Netty 4的新特性,当前版本还挺稳定,就暂时先不升级,以后看看再说。

坚守老版本还有很多其它的理由,例如考虑到线上系统的稳定性、对新版本的熟悉程度等。无论如何升级Netty都是一件大事,特别是对Netty有直接强依赖的产品。

从上面的分析可以看出,坚守老版本似乎是个不错的选择;但是,“理想是美好的,现实却是残酷的”,坚守老版本并非总是那么容易,下面我们就看下被迫升级的案例。

1.3. “被迫”升级到Netty 4.X

除了为了使用新特性而主动进行的版本升级,大多数升级都是“被迫的”。下面我们对这些升级原因进行分析。

  1. 公司的开源软件管理策略:对于那些大厂,不同部门和产品线依赖的开源软件版本经常不同,为了对开源依赖进行统一管理,降低安全、维护和管理成本,往往会指定优选的软件版本。由于Netty 4.X 系列版本已经非常成熟,因为,很多公司都优选Netty 4.X版本。
  2. 维护成本:无论是依赖Netty 3.X,还是Netty4.X,往往需要在原框架之上做定制。例如,客户端的短连重连、心跳检测、流控等。分别对Netty 4.X和3.X版本实现两套定制框架,开发和维护成本都非常高。根据开源软件的使用策略,当存在版本冲突的时候,往往会选择升级到更高的版本。对于Netty,依然遵循这个规则。
  3. 新特性:Netty 4.X相比于Netty 3.X,提供了很多新的特性,例如优化的内存管理池、对MQTT协议的支持等。如果用户需要使用这些新特性,最简便的做法就是升级Netty到4.X系列版本。
  4. 更优异的性能:Netty 4.X版本相比于3.X老版本,优化了内存池,减少了GC的频率、降低了内存消耗;通过优化Rector线程池模型,用户的开发更加简单,线程调度也更加高效。

1.4. 升级不当付出的代价

表面上看,类库包路径的修改、API的重构等似乎是升级的重头戏,大家往往把注意力放到这些“明枪”上,但真正隐藏和致命的却是“暗箭”。如果对Netty底层的事件调度机制和线程模型不熟悉,往往就会“中枪”。

本文以几个比较典型的真实案例为例,通过问题描述、问题定位和问题总结,让这些隐藏的“暗箭”不再伤人。

由于Netty 4线程模型改变导致的升级事故还有很多,限于篇幅,本文不一一枚举,这些问题万变不离其宗,只要抓住线程模型这个关键点,所谓的疑难杂症都将迎刃而解。

2. Netty升级之后遭遇内存泄露

2.1. 问题描述

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty4.X提供了基于内存池的缓冲区重用机制。性能测试表明,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

业务应用的特点是高并发、短流程,大多数对象都是朝生夕灭的短生命周期对象。为了减少内存的拷贝,用户期望在序列化的时候直接将对象编码到PooledByteBuf里,这样就不需要为每个业务消息都重新申请和释放内存。

业务的相关代码示例如下:

//在业务线程中初始化内存池分配器,分配非堆内存  ByteBufAllocator allocator = new PooledByteBufAllocator(true);  ByteBuf buffer = allocator.ioBuffer(1024); //构造订购请求消息并赋值,业务逻辑省略 SubInfoReq infoReq = new SubInfoReq (); infoReq.setXXX(......); //将对象编码到ByteBuf中 codec.encode(buffer, info); //调用ChannelHandlerContext进行消息发送 ctx.writeAndFlush(buffer);

业务代码升级Netty版本并重构之后,运行一段时间,Java进程就会宕机,查看系统运行日志发现系统发生了内存泄露(示例堆栈):

图2-1 OOM内存溢出堆栈

对内存进行监控(切换使用堆内存池,方便对内存进行监控),发现堆内存一直飙升,如下所示(示例堆内存监控):

图2-2 堆内存监控

2.2. 问题定位

使用jmap -dump:format=b,file=netty.bin PID 将堆内存dump出来,通过IBM的HeapAnalyzer工具进行分析,发现ByteBuf发生了泄露。

因为使用了内存池,所以首先怀疑是不是申请的ByteBuf没有被释放导致?查看代码,发现消息发送完成之后,Netty底层已经调用ReferenceCountUtil.release(message)对内存进行了释放。这是怎么回事呢?难道Netty 4.X的内存池有Bug,调用release操作释放内存失败?

考虑到Netty 内存池自身Bug的可能性不大,首先从业务的使用方式入手分析:

  1. 内存的分配是在业务代码中进行,由于使用到了业务线程池做I/O操作和业务操作的隔离,实际上内存是在业务线程中分配的;
  2. 内存的释放操作是在outbound中进行,按照Netty 3的线程模型,downstream(对应Netty 4的outbound,Netty 4取消了upstream和downstream)的handler也是由业务调用者线程执行的,也就是说释放跟分配在同一个业务线程中进行。

初次排查并没有发现导致内存泄露的根因,一筹莫展之际开始查看Netty的内存池分配器PooledByteBufAllocator的Doc和源码实现,发现内存池实际是基于线程上下文实现的,相关代码如下:

final ThreadLocal<PoolThreadCache> threadCache = new ThreadLocal<PoolThreadCache>() {         private final AtomicInteger index = new AtomicInteger();         @Override         protected PoolThreadCache initialValue() {             final int idx = index.getAndIncrement();             final PoolArena<byte[]> heapArena;             final PoolArena<ByteBuffer> directArena;             if (heapArenas != null) {                 heapArena = heapArenas[Math.abs(idx % heapArenas.length)];             } else {                 heapArena = null;             }             if (directArenas != null) {                 directArena = directArenas[Math.abs(idx % directArenas.length)];             } else {                 directArena = null;             }             return new PoolThreadCache(heapArena, directArena);         }

也就是说内存的申请和释放必须在同一线程上下文中,不能跨线程。跨线程之后实际操作的就不是同一块内存区域,这会导致很多严重的问题,内存泄露便是其中之一。内存在A线程申请,切换到B线程释放,实际是无法正确回收的。

通过对Netty内存池的源码分析,问题基本锁定。保险起见进行简单验证,通过对单条业务消息进行Debug,发现执行释放的果然不是业务线程,而是Netty的NioEventLoop线程:当某个消息被完全发送成功之后,会通过ReferenceCountUtil.release(message)方法释放已经发送成功的ByteBuf。

问题定位出来之后,继续溯源,发现Netty 4修改了Netty 3的线程模型:在Netty 3的时候,upstream是在I/O线程里执行的,而downstream是在业务线程里执行。当Netty从网络读取一个数据报投递给业务handler的时候,handler是在I/O线程里执行;而当我们在业务线程中调用write和writeAndFlush向网络发送消息的时候,handler是在业务线程里执行,直到最后一个Header handler将消息写入到发送队列中,业务线程才返回。

Netty4修改了这一模型,在Netty 4里inbound(对应Netty 3的upstream)和outbound(对应Netty 3的downstream)都是在NioEventLoop(I/O线程)中执行。当我们在业务线程里通过ChannelHandlerContext.write发送消息的时候,Netty 4在将消息发送事件调度到ChannelPipeline的时候,首先将待发送的消息封装成一个Task,然后放到NioEventLoop的任务队列中,由NioEventLoop线程异步执行。后续所有handler的调度和执行,包括消息的发送、I/O事件的通知,都由NioEventLoop线程负责处理。

下面我们分别通过对比Netty 3和Netty 4的消息接收和发送流程,来理解两个版本线程模型的差异:

Netty 3的I/O事件处理流程:

图2-3 Netty 3 I/O事件处理线程模型

Netty 4的I/O消息处理流程:

图2-4 Netty 4 I/O事件处理线程模型

2.3. 问题总结

Netty 4.X版本新增的内存池确实非常高效,但是如果使用不当则会导致各种严重的问题。诸如内存泄露这类问题,功能测试并没有异常,如果相关接口没有进行压测或者稳定性测试而直接上线,则会导致严重的线上问题。

内存池PooledByteBuf的使用建议:

  1. 申请之后一定要记得释放,Netty自身Socket读取和发送的ByteBuf系统会自动释放,用户不需要做二次释放;如果用户使用Netty的内存池在应用中做ByteBuf的对象池使用,则需要自己主动释放;
  2. 避免错误的释放:跨线程释放、重复释放等都是非法操作,要避免。特别是跨线程申请和释放,往往具有隐蔽性,问题定位难度较大;
  3. 防止隐式的申请和分配:之前曾经发生过一个案例,为了解决内存池跨线程申请和释放问题,有用户对内存池做了二次包装,以实现多线程操作时,内存始终由包装的管理线程申请和释放,这样可以屏蔽用户业务线程模型和访问方式的差异。谁知运行一段时间之后再次发生了内存泄露,最后发现原来调用ByteBuf的write操作时,如果内存容量不足,会自动进行容量扩展。扩展操作由业务线程执行,这就绕过了内存池管理线程,发生了“引用逃逸”。该Bug只有在ByteBuf容量动态扩展的时候才发生,因此,上线很长一段时间没有发生,直到某一天......因此,大家在使用Netty 4.X的内存池时要格外当心,特别是做二次封装时,一定要对内存池的实现细节有深刻的理解。

3. Netty升级之后遭遇数据被篡改

3.1. 问题描述

某业务产品,Netty3.X升级到4.X之后,系统运行过程中,偶现服务端发送给客户端的应答数据被莫名“篡改”。

业务服务端的处理流程如下:

  1. 将解码后的业务消息封装成Task,投递到后端的业务线程池中执行;
  2. 业务线程处理业务逻辑,完成之后构造应答消息发送给客户端;
  3. 业务应答消息的编码通过继承Netty的CodeC框架实现,即Encoder ChannelHandler;
  4. 调用Netty的消息发送接口之后,流程继续,根据业务场景,可能会继续操作原发送的业务对象。

业务相关代码示例如下:

//构造订购应答消息 SubInfoResp infoResp = new SubInfoResp(); //根据业务逻辑,对应答消息赋值 infoResp.setResultCode(0); infoResp.setXXX(); 后续赋值操作省略...... //调用ChannelHandlerContext进行消息发送 ctx.writeAndFlush(infoResp); //消息发送完成之后,后续根据业务流程进行分支处理,修改infoResp对象 infoResp.setXXX(); 后续代码省略......

3.2. 问题定位

首先对应答消息被非法“篡改”的原因进行分析,经过定位发现当发生问题时,被“篡改”的内容是调用writeAndFlush接口之后,由后续业务分支代码修改应答消息导致的。由于修改操作发生在writeAndFlush操作之后,按照Netty 3.X的线程模型不应该出现该问题。

在Netty3中,downstream是在业务线程里执行的,也就是说对SubInfoResp的编码操作是在业务线程中执行的,当编码后的ByteBuf对象被投递到消息发送队列之后,业务线程才会返回并继续执行后续的业务逻辑,此时修改应答消息是不会改变已完成编码的ByteBuf对象的,所以肯定不会出现应答消息被篡改的问题。

初步分析应该是由于线程模型发生变更导致的问题,随后查验了Netty 4的线程模型,果然发生了变化:当调用outbound向外发送消息的时候,Netty会将发送事件封装成Task,投递到NioEventLoop的任务队列中异步执行,相关代码如下:

@Override  public void invokeWrite(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {         if (msg == null) {             throw new NullPointerException("msg");         }         validatePromise(ctx, promise, true);         if (executor.inEventLoop()) {             invokeWriteNow(ctx, msg, promise);         } else {             AbstractChannel channel = (AbstractChannel) ctx.channel();             int size = channel.estimatorHandle().size(msg);             if (size > 0) {                 ChannelOutboundBuffer buffer = channel.unsafe().outboundBuffer();                 // Check for null as it may be set to null if the channel is closed already                 if (buffer != null) {                     buffer.incrementPendingOutboundBytes(size);                 }             }             safeExecuteOutbound(WriteTask.newInstance(ctx, msg, size, promise), promise, msg);         }     }

通过上述代码可以看出,Netty首先对当前的操作的线程进行判断,如果操作本身就是由NioEventLoop线程执行,则调用写操作;否则,执行线程安全的写操作,即将写事件封装成Task,放入到任务队列中由Netty的I/O线程执行,业务调用返回,流程继续执行。

通过源码分析,问题根源已经很清楚:系统升级到Netty 4之后,线程模型发生变化,响应消息的编码由NioEventLoop线程异步执行,业务线程返回。这时存在两种可能:

  1. 如果编码操作先于修改应答消息的业务逻辑执行,则运行结果正确;
  2. 如果编码操作在修改应答消息的业务逻辑之后执行,则运行结果错误。

由于线程的执行先后顺序无法预测,因此该问题隐藏的相当深。如果对Netty 4和Netty3的线程模型不了解,就会掉入陷阱。

Netty 3版本业务逻辑没有问题,流程如下:

图3-1 升级之前的业务流程线程模型

升级到Netty 4版本之后,业务流程由于Netty线程模型的变更而发生改变,导致业务逻辑发生问题:

图3-2 升级之后的业务处理流程发生改变

3.3. 问题总结

很多读者在进行Netty 版本升级的时候,只关注到了包路径、类和API的变更,并没有注意到隐藏在背后的“暗箭”- 线程模型变更。

升级到Netty 4的用户需要根据新的线程模型对已有的系统进行评估,重点需要关注outbound的ChannelHandler,如果它的正确性依赖于Netty 3的线程模型,则很可能在新的线程模型中出问题,可能是功能问题或者其它问题。

4. Netty升级之后性能严重下降

4.1. 问题描述

相信很多Netty用户都看过如下相关报告:

在Twitter,Netty 4 GC开销降为五分之一:Netty 3使用Java对象表示I/O事件,这样简单,但会产生大量的垃圾,尤其是在我们这样的规模下。Netty 4在新版本中对此做出了更改,取代生存周期短的事件对象,而以定义在生存周期长的通道对象上的方法处理I/O事件。它还有一个使用池的专用缓冲区分配器。

每当收到新信息或者用户发送信息到远程端,Netty 3均会创建一个新的堆缓冲区。这意味着,对应每一个新的缓冲区,都会有一个‘new byte[capacity]’。这些缓冲区会导致GC压力,并消耗内存带宽:为了安全起见,新的字节数组分配时会用零填充,这会消耗内存带宽。然而,用零填充的数组很可能会再次用实际的数据填充,这又会消耗同样的内存带宽。如果Java虚拟机(JVM)提供了创建新字节数组而又无需用零填充的方式,那么我们本来就可以将内存带宽消耗减少50%,但是目前没有那样一种方式。

在Netty 4中,代码定义了粒度更细的API,用来处理不同的事件类型,而不是创建事件对象。它还实现了一个新缓冲池,那是一个纯Java版本的 jemalloc (Facebook也在用)。现在,Netty不会再因为用零填充缓冲区而浪费内存带宽了。

我们比较了两个分别建立在Netty 3和4基础上echo协议服务器。(Echo非常简单,这样,任何垃圾的产生都是Netty的原因,而不是协议的原因)。我使它们服务于相同的分布式echo协议客户端,来自这些客户端的16384个并发连接重复发送256字节的随机负载,几乎使千兆以太网饱和。

根据测试结果,Netty 4:

  • GC中断频率是原来的1/5: 45.5 vs. 9.2次/分钟
  • 垃圾生成速度是原来的1/5: 207.11 vs 41.81 MiB/秒

正是看到了相关的Netty 4性能提升报告,很多用户选择了升级。事后一些用户反馈Netty 4并没有跟产品带来预期的性能提升,有些甚至还发生了非常严重的性能下降,下面我们就以某业务产品的失败升级经历为案例,详细分析下导致性能下降的原因。

4.2. 问题定位

首先通过JMC等性能分析工具对性能热点进行分析,示例如下(信息安全等原因,只给出分析过程示例截图):

图4-1 JMC性能监控分析

通过对热点方法的分析,发现在消息发送过程中,有两处热点:

  1. 消息发送性能统计相关Handler;
  2. 编码Handler。

对使用Netty 3版本的业务产品进行性能对比测试,发现上述两个Handler也是热点方法。既然都是热点,为啥切换到Netty4之后性能下降这么厉害呢?

通过方法的调用树分析发现了两个版本的差异:在Netty 3中,上述两个热点方法都是由业务线程负责执行;而在Netty 4中,则是由NioEventLoop(I/O)线程执行。对于某个链路,业务是拥有多个线程的线程池,而NioEventLoop只有一个,所以执行效率更低,返回给客户端的应答时延就大。时延增大之后,自然导致系统并发量降低,性能下降。

找出问题根因之后,针对Netty 4的线程模型对业务进行专项优化,性能达到预期,远超过了Netty 3老版本的性能。

Netty 3的业务线程调度模型图如下所示:充分利用了业务多线程并行编码和Handler处理的优势,周期T内可以处理N条业务消息。

图4-2 Netty 3业务调度性能模型

切换到Netty 4之后,业务耗时Handler被I/O线程串行执行,因此性能发生比较大的下降:

图4-3 Netty 4业务调度性能模型

4.3. 问题总结

该问题的根因还是由于Netty 4的线程模型变更引起,线程模型变更之后,不仅影响业务的功能,甚至对性能也会造成很大的影响。

对Netty的升级需要从功能、兼容性和性能等多个角度进行综合考虑,切不可只盯着API变更这个芝麻,而丢掉了性能这个西瓜。API的变更会导致编译错误,但是性能下降却隐藏于无形之中,稍不留意就会中招。

对于讲究快速交付、敏捷开发和灰度发布的互联网应用,升级的时候更应该要当心。

5. Netty升级之后上下文丢失

5.1. 问题描述

为了提升业务的二次定制能力,降低对接口的侵入性,业务使用线程变量进行消息上下文的传递。例如消息发送源地址信息、消息Id、会话Id等。

业务同时使用到了一些第三方开源容器,也提供了线程级变量上下文的能力。业务通过容器上下文获取第三方容器的系统变量信息。

升级到Netty 4之后,业务继承自Netty的ChannelHandler发生了空指针异常,无论是业务自定义的线程上下文、还是第三方容器的线程上下文,都获取不到传递的变量值。

5.2. 问题定位

首先检查代码,看业务是否传递了相关变量,确认业务传递之后怀疑跟Netty 版本升级相关,调试发现,业务ChannelHandler获取的线程上下文对象和之前业务传递的上下文不是同一个。这就说明执行ChannelHandler的线程跟处理业务的线程不是同一个线程!

查看Netty 4线程模型的相关Doc发现,Netty修改了outbound的线程模型,正好影响了业务消息发送时的线程上下文传递,最终导致线程变量丢失。

5.3. 问题总结

通常业务的线程模型有如下几种:

  1. 业务自定义线程池/线程组处理业务,例如使用JDK 1.5提供的ExecutorService;
  2. 使用J2EE Web容器自带的线程模型,常见的如JBoss和Tomcat的HTTP接入线程等;
  3. 隐式的使用其它第三方框架的线程模型,例如使用NIO框架进行协议处理,业务代码隐式使用的就是NIO框架的线程模型,除非业务明确的实现自定义线程模型。

在实践中我们发现很多业务使用了第三方框架,但是只熟悉API和功能,对线程模型并不清楚。某个类库由哪个线程调用,糊里糊涂。为了方便变量传递,又随意的使用线程变量,实际对背后第三方类库的线程模型产生了强依赖。当容器或者第三方类库升级之后,如果线程模型发生了变更,则原有功能就会发生问题。

鉴于此,在实际工作中,尽量不要强依赖第三方类库的线程模型,如果确实无法避免,则必须对它的线程模型有深入和清晰的了解。当第三方类库升级之后,需要检查线程模型是否发生变更,如果发生变化,相关的代码也需要考虑同步升级。

6. Netty3.X VS Netty4.X 之线程模型

通过对三个具有典型性的升级失败案例进行分析和总结,我们发现有个共性:都是线程模型改变惹的祸!

下面小节我们就详细得对Netty3和Netty4版本的I/O线程模型进行对比,以方便大家掌握两者的差异,在升级和使用中尽量少踩雷。

6.1 Netty 3.X 版本线程模型

Netty 3.X的I/O操作线程模型比较复杂,它的处理模型包括两部分:

  1. Inbound:主要包括链路建立事件、链路激活事件、读事件、I/O异常事件、链路关闭事件等;
  2. Outbound:主要包括写事件、连接事件、监听绑定事件、刷新事件等。

我们首先分析下Inbound操作的线程模型:

图6-1 Netty 3 Inbound操作线程模型

从上图可以看出,Inbound操作的主要处理流程如下:

  1. I/O线程(Work线程)将消息从TCP缓冲区读取到SocketChannel的接收缓冲区中;
  2. 由I/O线程负责生成相应的事件,触发事件向上执行,调度到ChannelPipeline中;
  3. I/O线程调度执行ChannelPipeline中Handler链的对应方法,直到业务实现的Last Handler;
  4. Last Handler将消息封装成Runnable,放入到业务线程池中执行,I/O线程返回,继续读/写等I/O操作;
  5. 业务线程池从任务队列中弹出消息,并发执行业务逻辑。

通过对Netty 3的Inbound操作进行分析我们可以看出,Inbound的Handler都是由Netty的I/O Work线程负责执行。

下面我们继续分析Outbound操作的线程模型:

图6-2 Netty 3 Outbound操作线程模型

从上图可以看出,Outbound操作的主要处理流程如下:

业务线程发起Channel Write操作,发送消息;

  1. Netty将写操作封装成写事件,触发事件向下传播;
  2. 写事件被调度到ChannelPipeline中,由业务线程按照Handler Chain串行调用支持Downstream事件的Channel Handler;
  3. 执行到系统最后一个ChannelHandler,将编码后的消息Push到发送队列中,业务线程返回;
  4. Netty的I/O线程从发送消息队列中取出消息,调用SocketChannel的write方法进行消息发送。

6.2 Netty 4.X 版本线程模型

相比于Netty 3.X系列版本,Netty 4.X的I/O操作线程模型比较简答,它的原理图如下所示:

图6-3 Netty 4 Inbound和Outbound操作线程模型

从上图可以看出,Outbound操作的主要处理流程如下:

  1. I/O线程NioEventLoop从SocketChannel中读取数据报,将ByteBuf投递到ChannelPipeline,触发ChannelRead事件;
  2. I/O线程NioEventLoop调用ChannelHandler链,直到将消息投递到业务线程,然后I/O线程返回,继续后续的读写操作;
  3. 业务线程调用ChannelHandlerContext.write(Object msg)方法进行消息发送;
  4. 如果是由业务线程发起的写操作,ChannelHandlerInvoker将发送消息封装成Task,放入到I/O线程NioEventLoop的任务队列中,由NioEventLoop在循环中统一调度和执行。放入任务队列之后,业务线程返回;
  5. I/O线程NioEventLoop调用ChannelHandler链,进行消息发送,处理Outbound事件,直到将消息放入发送队列,然后唤醒Selector,进而执行写操作。

通过流程分析,我们发现Netty 4修改了线程模型,无论是Inbound还是Outbound操作,统一由I/O线程NioEventLoop调度执行。

6.3. 线程模型对比

在进行新老版本线程模型PK之前,首先还是要熟悉下串行化设计的理念:

我们知道当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。

为了解决上述问题,Netty 4采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由I/O线程NioEventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险,对于用户而言,甚至不需要了解Netty的线程细节,这确实是个非常好的设计理念,它的工作原理图如下:

图6-4 Netty 4的串行化设计理念

一个NioEventLoop聚合了一个多路复用器Selector,因此可以处理成百上千的客户端连接,Netty的处理策略是每当有一个新的客户端接入,则从NioEventLoop线程组中顺序获取一个可用的NioEventLoop,当到达数组上限之后,重新返回到0,通过这种方式,可以基本保证各个NioEventLoop的负载均衡。一个客户端连接只注册到一个NioEventLoop上,这样就避免了多个I/O线程去并发操作它。

Netty通过串行化设计理念降低了用户的开发难度,提升了处理性能。利用线程组实现了多个串行化线程水平并行执行,线程之间并没有交集,这样既可以充分利用多核提升并行处理能力,同时避免了线程上下文的切换和并发保护带来的额外性能损耗。

了解完了Netty 4的串行化设计理念之后,我们继续看Netty 3线程模型存在的问题,总结起来,它的主要问题如下:

  1. Inbound和Outbound实质都是I/O相关的操作,它们的线程模型竟然不统一,这给用户带来了更多的学习和使用成本;
  2. Outbound操作由业务线程执行,通常业务会使用线程池并行处理业务消息,这就意味着在某一个时刻会有多个业务线程同时操作ChannelHandler,我们需要对ChannelHandler进行并发保护,通常需要加锁。如果同步块的范围不当,可能会导致严重的性能瓶颈,这对开发者的技能要求非常高,降低了开发效率;
  3. Outbound操作过程中,例如消息编码异常,会产生Exception,它会被转换成Inbound的Exception并通知到ChannelPipeline,这就意味着业务线程发起了Inbound操作!它打破了Inbound操作由I/O线程操作的模型,如果开发者按照Inbound操作只会由一个I/O线程执行的约束进行设计,则会发生线程并发访问安全问题。由于该场景只在特定异常时发生,因此错误非常隐蔽!一旦在生产环境中发生此类线程并发问题,定位难度和成本都非常大。

讲了这么多,似乎Netty 4 完胜 Netty 3的线程模型,其实并不尽然。在特定的场景下,Netty 3的性能可能更高,就如本文第4章节所讲,如果编码和其它Outbound操作非常耗时,由多个业务线程并发执行,性能肯定高于单个NioEventLoop线程。

但是,这种性能优势不是不可逆转的,如果我们修改业务代码,将耗时的Handler操作前置,Outbound操作不做复杂业务逻辑处理,性能同样不输于Netty 3,但是考虑内存池优化、不会反复创建Event、不需要对Handler加锁等Netty 4的优化,整体性能Netty 4版本肯定会更高。

总而言之,如果用户真正熟悉并掌握了Netty 4的线程模型和功能类库,相信不仅仅开发会更加简单,性能也会更优!

6.4. 思考

就Netty 而言,掌握线程模型的重要性不亚于熟悉它的API和功能。很多时候我遇到的功能、性能等问题,都是由于缺乏对它线程模型和原理的理解导致的,结果我们就以讹传讹,认为Netty 4版本不如3好用等。

不能说所有开源软件的版本升级一定都胜过老版本,就Netty而言,我认为Netty 4版本相比于老的Netty 3,确实是历史的一大进步。

7. 作者简介

李林锋,2007年毕业于东北大学,2008年进入华为公司从事高性能通信软件的设计和开发工作,有7年NIO设计和开发经验,精通Netty、Mina等NIO框架和平台中间件,现任华为软件平台架构部架构师,《Netty权威指南》作者。

联系方式:新浪微博 Nettying 微信:Nettying 微信公众号:Netty之家


感谢郭蕾对本文的策划和审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

posted @ 2015-02-10 12:03 小马歌 阅读(919) | 评论 (0)编辑 收藏
 

这个帖子是关于JAVA中鲜为人知的特性的后续更新,如果想得到下次在线讨论的更新,请通过邮件订阅,并且不要忘了在评论区留下你的意见和建议。


    Java是一个安全的开发工具,它阻止开发人员犯很多低级的错误,而大部份的错误都是基于内存管理方面的。如果你想搞破坏,可以使用Unsafe这个类。这个类是属于sun.* API中的类,并且它不是J2SE中真正的一部份,因此你可能找不到任何的官方文档,更可悲的是,它也没有比较好的代码文档。


    实例化sun.misc.Unsafe

    如果你尝试创建Unsafe类的实例,基于以下两种原因是不被允许的。

    1)、Unsafe类的构造函数是私有的;

    2)、虽然它有静态的getUnsafe()方法,但是如果你尝试调用Unsafe.getUnsafe(),会得到一个SecutiryException。这个类只有被JDK信任的类实例化。

    但是这总会是有变通的解决办法的,一个简单的方式就是使用反射进行实例化:

  1. Field f = Unsafe.class.getDeclaredField("theUnsafe"); //Internal reference  
  2. f.setAccessible(true);  
  3. Unsafe unsafe = (Unsafe) f.get(null);  

    注:IDE如Eclipse对会这样的使用报错,不过不用担心,直接运行代码就行,可以正常运行的。

    (译者注:还有一种解决方案,就是将Eclipse中这种限制获取由错误,修改为警告,具体操作为将Windows->Preference...->Java->Compiler->Errors/Warnings中的"Deprecated and restricted API",级别由Error修改为Warning就可以了)

    现在进入主题,使用这个对象我们可以做如下“有趣的”事情。


    使用sun.misc.Unsafe

    1)、突破限制创建实例

    通过allocateInstance()方法,你可以创建一个类的实例,但是却不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,我们也可以通过这个方法创建它的实例。

    (这个对单例模式情有独钟的程序员来说将会是一个噩梦,它们没有办法阻止这种方式调用大笑

    看下面一个实例(注:为了配合这个主题,译者将原实例中的public构造函数修改为了私有的):

 

  1. public class UnsafeDemo {  
  2.     public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, InstantiationException {  
  3.         Field f = Unsafe.class.getDeclaredField("theUnsafe"); // Internal reference  
  4.         f.setAccessible(true);  
  5.         Unsafe unsafe = (Unsafe) f.get(null);  
  6.   
  7.         // This creates an instance of player class without any initialization  
  8.         Player p = (Player) unsafe.allocateInstance(Player.class);  
  9.         System.out.println(p.getAge()); // Print 0  
  10.   
  11.         p.setAge(45); // Let's now set age 45 to un-initialized object  
  12.         System.out.println(p.getAge()); // Print 45  
  13.     }  
  14. }  
  15.   
  16. class Player {  
  17.     private int age = 12;  
  18.   
  19.     private Player() {  
  20.         this.age = 50;  
  21.     }  
  22.   
  23.     public int getAge() {  
  24.         return this.age;  
  25.     }  
  26.   
  27.     public void setAge(int age) {  
  28.         this.age = age;  
  29.     }  
  30. }  


    2)、使用直接获取内存的方式实现浅克隆

    如何实现浅克隆?在clone(){...}方法中调用super.clone(),对吗?这里存在的问题是首先你必须继续Cloneable接口,并且在所有你需要做浅克隆的对象中实现clone()方法,对于一个懒懒的程序员来说,这个工作量太大了。

    我不推荐上面的做法而是直接使用Unsafe,我们可以仅使用几行代码就实现浅克隆,并且它可以像某些工具类一样用于任意类的克隆。

    这个戏法就是把一个对象的字节码拷贝到内存的另外一个地方,然后再将这个对象转换为被克隆的对象类型。

    

    3)、来自黑客的密码安全

    这个好似很有趣吧?实事就是这样的。开发人员创建密码或者是保证密码到字符串中,然后在应用程序的代码中使用这些密码,使用过后,聪明的程序员会把字符串的引用设为NULL,因此它就不会被引用着并且很容易被垃圾收集器给回收掉。

    但是从你将引用设为NULL到被垃圾收集器收集的这个时间段之内(原文:But from the time, you made the reference null to the time garbage collector kicks in),它是处于字符串池中的,并且在你系统中进行一个复杂的攻击(原文:And a sophisticated attack on your system),也是可以读取到你的内存区域并且获得密码,虽然机会很小,但是总是存在的。

    这就是为什么建议使用char[]数组存放密码,当使用完过后,你可以迭代处理当前数组,修改/清空这些字符。

    另外一个方式就是使用魔术类Unsafe。你可以创建另外一个和当前密码字符串具有相同长度的临时字符串,将临时密码中的每个字符都设值为"?"或者"*"(任何字符都可以),当你完成密码的逻辑后,你只需要简单的将临时密码中的字节数组拷贝到原始的密码串中,这就是使用临时密码覆盖真实的密码。

    示例代码可能会是这样:

  1. String password = new String("l00k@myHor$e");  
  2. String fake = new String(password.replaceAll(".", "?"));  
  3. System.out.println(password); // l00k@myHor$e  
  4. System.out.println(fake); // ????????????  
  5.    
  6. getUnsafe().copyMemory(fake, 0L, null, toAddress(password), sizeOf(password));  
  7.    
  8. System.out.println(password); // ????????????  
  9. System.out.println(fake); // ????????????  

    运行时动态创建类

    我们可以在运行时运态的创建类,例如通过编译后的.class文件,操作方式就是将.class文件读取到字节数据组中,并将其传到defineClass方法中。

  1. //Sample code to craeet classes  
  2. byte[] classContents = getClassContent();  
  3. Class c = getUnsafe().defineClass(null, classContents, 0, classContents.length);  
  4. c.getMethod("a").invoke(c.newInstance(), null);  
  5.    
  6. //Method to read .class file  
  7. private static byte[] getClassContent() throws Exception {  
  8.     File f = new File("/home/mishadoff/tmp/A.class");  
  9.     FileInputStream input = new FileInputStream(f);  
  10.     byte[] content = new byte[(int)f.length()];  
  11.     input.read(content);  
  12.     input.close();  
  13.     return content;  
  14. }  
    

    4)、超大数组

    从所周知,常量Integer.MAX_VALUE是JAVA中数组长度的最大值,如果你想创建一个非常大的数组(虽然在通常的应用中不可能会用上),可以通过对内存进行直接分配实现。

    下面这个示例将会创建分配一段连续的内存(数组),它的容易是允许最大容量的两倍。

  1. class SuperArray {  
  2.     private final static int BYTE = 1;  
  3.     private long size;  
  4.     private long address;  
  5.        
  6.     public SuperArray(long size) {  
  7.         this.size = size;  
  8.         //得到分配内存的起始地址  
  9.         address = getUnsafe().allocateMemory(size * BYTE);  
  10.     }  
  11.     public void set(long i, byte value) {  
  12.         getUnsafe().putByte(address + i * BYTE, value);  
  13.     }  
  14.     public int get(long idx) {  
  15.         return getUnsafe().getByte(address + idx * BYTE);  
  16.     }  
  17.     public long size() {  
  18.         return size;  
  19.     }  
  20. }  
    应用示例

  1. long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;  
  2. SuperArray array = new SuperArray(SUPER_SIZE);  
  3. System.out.println("Array size:" + array.size()); // 4294967294  
  4. for (int i = 0; i < 100; i++) {  
  5.     array.set((long)Integer.MAX_VALUE + i, (byte)3);  
  6.     sum += array.get((long)Integer.MAX_VALUE + i);  
  7. }  
  8. System.out.println("Sum of 100 elements:" + sum);  // 300  

    但请注意这可能会导致JVM挂掉。

    

    结束语

    sun.misc.Unsafe provides almost unlimited capabilities for exploring and modification of VM’s runtime data structures. Despite the fact that these capabilities are almost inapplicable in Java development itself, Unsafe is a great tool for anyone who want to study HotSpot VM without C++ code debugging or need to create ad hoc profiling instruments.

    sun.misc.Unsafe提供了可以随意查看及修改JVM中运行时的数据结构,尽管这些功能在JAVA开发本身是不适用的,Unsafe是一个用于研究学习HotSpot虚拟机非常棒的工具,因为它不需要调用C++代码,或者需要创建即时分析的工具。


    参考

    http://mishadoff.github.io/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/



posted @ 2015-02-04 15:12 小马歌 阅读(274) | 评论 (0)编辑 收藏
 
环境:
os: MAC OS X 10.10.1 yosemite
target build OPENJDK:jdk8
Xcode:6.1
LLVM Version:

Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)

Target: x86_64-apple-darwin14.0.0

Thread model: posix

步骤:
1 国内hg拉取代码不靠谱,到这里打包下载,118M  
http://download.java.net/openjdk/jdk8/

安装X11,系统默认好像是没有这个东西,需要下载安装XQuartz,然后link下 sudo ln -s /usr/X11/include/X11 /usr/include/X11

sudo ln -s /usr/bin/llvm-g++ /Applications/Xcode.app/Contents/Developer/usr/bin/llvm-g++  
sudo ln -s /usr/bin/llvm-gcc /Applications/Xcode.app/Contents/Developer/usr/bin/llvm-gcc
4 安装
Xcode的Command line tools、freetype
./configure --enable-debug --with-target-bits=64
6 cd jdk8;
unset JAVA_HOME
unset CLASSPATH 
make CC=clang COMPILER_WARNINGS_FATAL=false LFLAGS='-Xlinker -lstdc++' USE_CLANG=true LP64=1 LANG=C ALT_BOOTDIR=/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home ARCH_DATA_MODEL=64 HOTSPOT_BUILD_JOBS=8 ALT_FREETYPE_HEADERS_PATH=/Users/mungo/Downloads/work/homebrew-master/Cellar/freetype/2.5.5/include ALT_FREETYPE_LIB_PATH=/Users/mungo/Downloads/work/homebrew-master/Cellar/freetype/2.5.5/lib
7 验证结果:

./build/macosx-x86_64-normal-server-fastdebug/jdk/bin/java -version

openjdk version "1.8.0-internal-fastdebug"

OpenJDK Runtime Environment (build 1.8.0-internal-fastdebug-mungo_2015_01_29_16_11-b00)

OpenJDK 64-Bit Server VM (build 25.0-b70-fastdebug, mixed mode)


出现的问题:

clang: error: unknown argument: '-fcheck-new' [-Wunused-command-line-argument-hard-error-in-future]  

         于是直接打开YourOpenJDK/hotspot/make/bsd/makefiles/gcc.make,把这行(line 193)给注释掉
2 relocInfo.hpp错误。 将接口的默认值去掉,把默认值放到的方法参数里即可。hotspot/src/share/vm/code/relocInfo.hpp 
+ inline friend relocInfo prefix_relocInfo(int datalen);

+inline relocInfo prefix_relocInfo(int datalen = 0) {
   assert(relocInfo::fits_into_immediate(datalen), "datalen in limits");
   return relocInfo(relocInfo::data_prefix_tag, relocInfo::RAW_BITS, relocInfo::datalen_tag | datalen);
 }

参考文章:
http://hllvm.group.iteye.com/group/topic/39814
http://yueyemaitian.iteye.com/blog/2038304

另:openjdk 7 hotspot我可以编译成功,但是最后打包的时候,出现 执行cp 错误.没找到原因

posted @ 2015-01-30 10:06 小马歌 阅读(628) | 评论 (0)编辑 收藏
 
     摘要: 这是我的WWDC2013系列笔记中的一篇,完整的笔记列表请参看这篇总览。本文仅作为个人记录使用,也欢迎在许可协议范围内转载或使用,但是还烦请保留原文链接,谢谢您的理解合作。如果您觉得本站对您能有帮助,您可以使用RSS或邮件方式订阅本站,这样您将能在第一时间获取本站信息。本文涉及到的WWDC2013 Session有Session 204 What's New with MultitaskingSe...  阅读全文
posted @ 2015-01-19 18:11 小马歌 阅读(202) | 评论 (0)编辑 收藏
 

接近一周没更新《Java线程》专栏了,主要是这周工作上比较忙,生活上也比较忙,呵呵,进入正题,上一篇讲述了并发包下的Lock,Lock可以更好的解决线程同步问题,使之更面向对象,并且ReadWriteLock在处理同步时更强大,那么同样,线程间仅仅互斥是不够的,还需要通信,本篇的内容是基于上篇之上,使用Lock如何处理线程通信。

        那么引入本篇的主角,Condition,Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。下面将之前写过的一个线程通信的例子替换成用Condition实现(Java线程(三)),代码如下:

  1. public class ThreadTest2 {  
  2.     public static void main(String[] args) {  
  3.         final Business business = new Business();  
  4.         new Thread(new Runnable() {  
  5.             @Override  
  6.             public void run() {  
  7.                 threadExecute(business, "sub");  
  8.             }  
  9.         }).start();  
  10.         threadExecute(business, "main");  
  11.     }     
  12.     public static void threadExecute(Business business, String threadType) {  
  13.         for(int i = 0; i < 100; i++) {  
  14.             try {  
  15.                 if("main".equals(threadType)) {  
  16.                     business.main(i);  
  17.                 } else {  
  18.                     business.sub(i);  
  19.                 }  
  20.             } catch (InterruptedException e) {  
  21.                 e.printStackTrace();  
  22.             }  
  23.         }  
  24.     }  
  25. }  
  26. class Business {  
  27.     private boolean bool = true;  
  28.     private Lock lock = new ReentrantLock();  
  29.     private Condition condition = lock.newCondition();   
  30.     public /*synchronized*/ void main(int loop) throws InterruptedException {  
  31.         lock.lock();  
  32.         try {  
  33.             while(bool) {                 
  34.                 condition.await();//this.wait();  
  35.             }  
  36.             for(int i = 0; i < 100; i++) {  
  37.                 System.out.println("main thread seq of " + i + ", loop of " + loop);  
  38.             }  
  39.             bool = true;  
  40.             condition.signal();//this.notify();  
  41.         } finally {  
  42.             lock.unlock();  
  43.         }  
  44.     }     
  45.     public /*synchronized*/ void sub(int loop) throws InterruptedException {  
  46.         lock.lock();  
  47.         try {  
  48.             while(!bool) {  
  49.                 condition.await();//this.wait();  
  50.             }  
  51.             for(int i = 0; i < 10; i++) {  
  52.                 System.out.println("sub thread seq of " + i + ", loop of " + loop);  
  53.             }  
  54.             bool = false;  
  55.             condition.signal();//this.notify();  
  56.         } finally {  
  57.             lock.unlock();  
  58.         }  
  59.     }  
  60. }  
        在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。

        这样看来,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建立不同的Condition,下面引入API中的一段代码,加以说明。

  1. class BoundedBuffer {  
  2.    final Lock lock = new ReentrantLock();//锁对象  
  3.    final Condition notFull  = lock.newCondition();//写线程条件   
  4.    final Condition notEmpty = lock.newCondition();//读线程条件   
  5.   
  6.    final Object[] items = new Object[100];//缓存队列  
  7.    int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;  
  8.   
  9.    public void put(Object x) throws InterruptedException {  
  10.      lock.lock();  
  11.      try {  
  12.        while (count == items.length)//如果队列满了   
  13.          notFull.await();//阻塞写线程  
  14.        items[putptr] = x;//赋值   
  15.        if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0  
  16.        ++count;//个数++  
  17.        notEmpty.signal();//唤醒读线程  
  18.      } finally {  
  19.        lock.unlock();  
  20.      }  
  21.    }  
  22.   
  23.    public Object take() throws InterruptedException {  
  24.      lock.lock();  
  25.      try {  
  26.        while (count == 0)//如果队列为空  
  27.          notEmpty.await();//阻塞读线程  
  28.        Object x = items[takeptr];//取值   
  29.        if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0  
  30.        --count;//个数--  
  31.        notFull.signal();//唤醒写线程  
  32.        return x;  
  33.      } finally {  
  34.        lock.unlock();  
  35.      }  
  36.    }   
  37.  }  
        这是一个处于多线程工作环境下的缓存区,缓存区提供了两个方法,put和take,put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码,这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据,其缓存队列(先进先出后进后出)能缓存的最大数值是100,多个线程间是互斥的,当缓存队列中存储的值达到100时,将写线程阻塞,并唤醒读线程,当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程,下面分析一下代码的执行过程:

        1. 一个写线程执行,调用put方法;

        2. 判断count是否为100,显然没有100;

        3. 继续执行,存入值;

        4. 判断当前写入的索引位置++后,是否和100相等,相等将写入索引值变为0,并将count+1;

        5. 仅唤醒读线程阻塞队列中的一个;

        6. 一个读线程执行,调用take方法;

        7. ……

        8. 仅唤醒写线程阻塞队列中的一个。

        这就是多个Condition的强大之处,假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程,那么假设只有一个Condition会有什么效果呢,缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

        本文来自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7481142,转载请注明。

    posted @ 2015-01-15 09:36 小马歌 阅读(214) | 评论 (0)编辑 收藏
     

    Gitlab是一个用Ruby on Rails开发的开源项目管理程序,可以通过WEB界面进行访问公开的或者私人项目。它和Github有类似的功能,能够浏览源代码,管理缺陷和注释。

    下面介绍如何在 Debian/Ubuntu 和 Centos 下搭建配置 GitLab。

    安装依赖

    Debian/Ubuntu下:

    sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate 

    安装python(注意需要2.5以上版本):

    sudo apt-get install -y python python-docutils 

    安装git(注意需要1.7.10以上版本):

    sudo apt-get install -y git-core 

    Centos下官方仓库的软件比较老旧,推荐先添加epel源,然后再安装依赖:

    sudo yum install git patch gcc-c++ readline-devel zlib-devel libffi-devel openssl-devel make autoconf automake libtool bison libxml2-devel libxslt-devel libyaml-devel git python python-docutils 

    安装 Ruby 2.0

    需要安装Ruby2.0,软件仓库中的Ruby 1.8不支持:

    mkdir /tmp/ruby && cd /tmp/ruby curl --progress ftp://ftp.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p353.tar.gz | tar xz cd ruby-2.0.0-p353 ./configure --disable-install-rdoc make sudo make install 

    安装Bundler Gem:

    sudo gem install bundler --no-ri --no-rdoc 

    配置gitlab-shell

    创建git用户:

    sudo adduser --system --create-home --comment 'GitLab' git   

    配置gitlab-shell

    su - git -c "git clone https://github.com/gitlabhq/gitlab-shell.git"   su - git -c "cd gitlab-shell && git checkout v1.3.0"   su - git -c "cp gitlab-shell/config.yml.example gitlab-shell/config.yml"   sed -i "s/localhost/gitlab.51yip.com/g" /home/git/gitlab-shell/config.yml   su - git -c "gitlab-shell/bin/install"   chmod 600 /home/git/.ssh/authorized_keys   chmod 700 /home/git/.ssh 

    数据库

    GitLab支持 MySQL 和 PostgreSQL 数据库。下面以 MySQL为例,介绍安装方法:

    Debian/Ubuntu下使用如下命令安装:

    sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev 

    Centos下使用如下命令:

    sudo yum install mysql-server  sudo chkconfig mysqld on 

    配置MySQL:

    sudo echo "CREATE DATABASE IF NOT EXISTS gitlabhq_production DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_unicode_ci';" | mysql -u root  sudo echo "UPDATE mysql.user SET Password=PASSWORD('123456') WHERE User='root'; FLUSH PRIVILEGES;" | mysql -u root  

    注意,用你的密码替换123456

    安装配置 gitlab

    su - git -c "git clone https://github.com/gitlabhq/gitlabhq.git gitlab"   su - git -c "cd gitlab;git checkout 5-1-stable"   su git -c "cp config/gitlab.yml.example config/gitlab.yml"   su git -c "mkdir /home/git/gitlab-satellites"   su git -c "mkdir public/uploads"   su git -c "mkdir -p tmp/sockets/"   su git -c "mkdir -p tmp/pids/"   sed -i "s/ host: localhost/ host: gitlab.segmentfault.com/g" config/gitlab.yml   sed -i "s/from: gitlab@localhost/from: gitlab@gitlab.segmentfault.com/g" config/gitlab.yml   su git -c "cp config/puma.rb.example config/puma.rb"   su git -c 'git config --global user.name "GitLab"'   su git -c 'git config --global user.email "gitlab@gitlab.segmentfault.com"' 

    注意将gitlab.segmentfault.com替换为你自己的内容。

    配置数据库连接:

    sudo su git -c "cp config/database.yml.mysql config/database.yml" sudo sed -i "s/secure password/mysql的root密码/g" config/database.yml 

    安装MySQL需要的Gems

    sudo -u git -H bundle install --deployment --without development test postgres aws 

    初始化:

    sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab sudo chmod +x /etc/init.d/gitlab sudo update-rc.d gitlab defaults 21 

    查看是否配置妥当:

    sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production 

    重启GitLab:

    sudo service gitlab start 

    配置Nginx

    Debian/Ubuntu下:

    sudo apt-get install -y nginx 

    CentOS下:

    sudo yum install nginx 

    下载配置文件样例:

    sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab 

    修改 /etc/nginx/sites-available/gitlab,特别留意将 YOUR_SERVER_FQDN 改成自己的。

    重启nginx:

    sudo service nginx restart 

    好了,你可以登录GitLab了,默认安装后的用户名:admin@local.host,密码5iveL!fe

    posted @ 2015-01-14 14:44 小马歌 阅读(653) | 评论 (0)编辑 收藏
    仅列出标题
    共95页: First 上一页 13 14 15 16 17 18 19 20 21 下一页 Last