Tag Archives: Java

优化 Java 反射字段访问

最近负责的一个 Java 服务经常报 CPU 占用高的告警。于是乘着一次 CPU 高的时候,去用 JFR profile 了一下。(使用 JFR 对 Java 应用 profile 可以参考这篇文章:Java 应用在线性能分析和火焰图
)。把数据拖回来用 JMC 打开一看,发现热点都在反射上:

reflect_jfr

图里排在最前面的都是反射相关的函数,而且实际都是同一个地方引入的。那里为了读取一个私有字段,使用了类似下面的代码:

public class SomeSingleton {
    private Field field;

    public SomeSingleton() {
        field = someObject.getDeclaredField("fieldName");
        field.setAccessible(true);
    }


    private void someMethod() {
        Object value = field.get(someObject);
    }
}

这里已经 SomeSingleton 是个单例对象,field 只会反射一次,而且 setAccessible(true) 后已经避免了额外的访问权限检查。profile 出来的性能问题集中在 field.get(someObject) 上。

于是去看了下这块 Java 反射的源代码,发现一个有意思的地方。

Field.get 会创建并缓存一个 FieldAccessor 对象,通过这个对象访问实际的字段。而这个 FieldAccessor 对象会由于对象的修饰符不同而创建不同的实例(UnsafeFieldAccessorFactory.java)代码大致如下:

class UnsafeFieldAccessorFactory {
    static FieldAccessor newFieldAccessor(Field field, boolean override) {
        Class<?> type = field.getType();
        boolean isStatic = Modifier.isStatic(field.getModifiers());
        boolean isFinal = Modifier.isFinal(field.getModifiers());
        boolean isVolatile = Modifier.isVolatile(field.getModifiers());
        boolean isQualified = isFinal || isVolatile;
        boolean isReadOnly = isFinal && (isStatic || !override);
        if (isStatic) {
            ...
            if (!isQualified) {
                ...
                } else {
                    return new UnsafeStaticObjectFieldAccessorImpl(field);
                }
            } else {
                ...
                } else {
                    return new UnsafeQualifiedStaticObjectFieldAccessorImpl(field, isReadOnly);
                }
            }
        } else {
            if (!isQualified) {
                ...
                } else {
                    return new UnsafeObjectFieldAccessorImpl(field);
                }
            } else {
                ...
                } else {
                    return new UnsafeQualifiedObjectFieldAccessorImpl(field, isReadOnly);
                }
            }
        }
    }
}

这里有四种情况的组合,有/没有 Static,有/没有 Qualified。

有 Qualified 的实现,对应 volatile 或 final 的字段,会忽略线程本地 cache。没有 Static 的那组,即访问的是实例对象,会有一次额外的检查对象实例是否匹配。即前面截图中的 ensureObject。(参见:UnsafeObjectFieldAccessorImpl.java

我们访问的是一个实例对象,所以每次访问字段都经过了这一次实际并不需要的检查。为了优化这个操作,不得不用上了 unsafe 大法,写了个加速反射字段访问的工具类。

...

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeFieldAccessor {
    private static final Unsafe unsafe ;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private final long fieldOffset;

    public UnsafeFieldAccessor(Field field) {
        fieldOffset = unsafe.objectFieldOffset(field);
    }

    public Object getObject(Object object) {
        return unsafe.getObject(object, fieldOffset);
    }

    public void setObject(Object object, Object value) {
        unsafe.putObject(object, fieldOffset, value);
    }

    public int getInt(Object object) {
        return unsafe.getInt(object, fieldOffset);
    }

    public void setInt(Object object, int value) {
        unsafe.putInt(object, fieldOffset, value);
    }
}

这里是 Java8 的写法。Java9 及以上版本 Unsafe 的使用需要做一定调整。这里有 Object 和 int 类型的字段访问,其他基本类型,如 long, double ,或者需要触发清掉 cache 也可以依样画葫芦。

下面对比一下性能。在我自己的机器上,各自执行 10000000 次,通过反射和 UnsafeFieldAccessor 的时间。

测试代码:

public class UnsafeFieldAccessorTest {
    static class T {
        private Object obj;
    }

    public void benchmark() throws Exception {
        Field field = T.class.getDeclaredField("obj");
        field.setAccessible(true);
        UnsafeFieldAccessor accessor = new UnsafeFieldAccessor(field);
        T t = new T();
        Object v = new Object();
        int n = 10000000;
        long start;


        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            accessor.getObject(t);
        }
        System.out.printf("unsafe get: %d\n", System.nanoTime() - start);

        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            field.get(t);
        }
        System.out.printf("reflect get: %d\n", System.nanoTime() - start);

        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            accessor.setObject(t, v);
        }
        System.out.printf("unsafe set: %d\n", System.nanoTime() - start);

        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            field.set(t, v);
        }
        System.out.printf("reflect set: %d\n", System.nanoTime() - start);
    }
}

结果如下,单位是纳秒:

unsafe get: 6859493
reflect get: 22821121
unsafe set: 10714617
reflect set: 53694089

在能确保赋值的类型安全的前提下,使用 unsafe 绕过一些检查,可以大幅减少反射访问字段消耗的时间。

Java 应用在线性能分析和火焰图

在碰到线上性能问题的时候,如果能在线通过采样方式获取热点函数/方法就可以更方便地定位问题所在,进行优化。采用在线采样的方式,由于性能影响小,可以比较放心地在线上进行,获取第一手数据。Linux 平台上,对于多数 C/C++ 编写的应用,可以通过 perf 来方便的采样,还可以进一步生成火焰图来更直观地观察。Java 是没法直接用 perf 的。虽然有一个 perf-map-agent,但是并不方便,尝试过程中还弄出了 kernel panic,所以这玩意是不敢在线上用了。不过 JDK 自己其实已经带了一个采样工具 FlightRecorder ,算是 JMC 的一部分。在 Java 11 以及之后版本的 OpenJDK 中以及可以随意使用。但是在之前版本中,是只有 Oracle JDK 才有的,还算商业版本的功能,虽说启用的时候并没检查 license,但是理论上要在生产环境用,还是要花钱的。

首先,应用启动的时候,要给 java 加上参数:

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:FlightRecorderOptions=loglevel=info

因为 JVM 默认在 safepoint 的地方才可以返回栈,所以最好加上

-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints

让 JVM 在非 safepoint 的时候也提供原数据。

然后在想开始采样的时候

sudo -u <java_user> -i jcmd <pid> JFR.start filename=/tmp/app.jfr duration=60s  settings=profile

这里可以指定输出的文件路径,和采样时间。之后可以用

sudo -u <java_user> -i jcmd <pid> JFR.check

来检查采样是不是已经完成了。

此处的 settings=profile 会增加一些性能指标的采样,比如堆内对象,IO 等。默认是 default。这里的 profile、default 实际是配置文件名,位于 $JAVA_HOME/jre/lib/jfr 下。也可以创建自己的配置。

更详细的用法可以参考官方文档:https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/toc.htm

确认完成后,就可以把 jfr 文件传回本地,用 jmc 来分析了。如果想要生成火焰图,还有这么个工具:https://github.com/chrishantha/jfr-flame-graph。具体用法可以看文档。大致上,代码拖回来后,

cd jfr-flame-graph
install-mc-jars.sh
mvn clean install -U

就编译好了。另外还需要 https://github.com/brendangregg/FlameGraph

工具准备好以后,

path/to/jfr-flame-graph/run.sh -f app.jfr -o app.txt
cat app.txt | path/to/FlameGraph/flamegraph.pl >app.svg

就能生成一个漂亮的火焰图了。

参考链接

  1. https://docs.oracle.com/javacomponents/jmc-5-5/jfr-command-reference/JFRCR.pdf
  2. https://docs.oracle.com/javacomponents/jmc-5-5/jfr-command-reference/diagnostic-command-reference.htm

php的interface == 鸡肋?

话说当年,php5诞生的时候,向java,c++学了好多面向对象的语法元素。其中就有interface。在java圈子里,主流的观念是要面向接口编程而不是面向类编程,将接口与实现分离。记得在Spring in Action的例子里,要创建一个骑士对象,首先写一个Knight接口,里面定义了骑士可以做什么。具体怎么做则是在类KnightImpl里实现的。看上去似乎有一点麻烦。不过java大牛们教育我们,这是为了更好的灵活性。这样使用骑士的时候不用关心这是个什么样的骑士,而且可以在不修改代码,只修改工厂的配置文件,就可以用另一个实现来替换现有的实现。更寻常的说法是,比如Map接口,通常我们使用一个映射表的时候只要能够满足键值对应的操作就行。我们可以根据具体需要选用哈希表实现或是查找树实现,甚至自己定义一个存放在数据库中的Map。简单的说,接口定义了一组操作,实现了某个接口的对象也就能提供这个接口规定的一组操作。接口就像是一个协议或者说一个标准。这么说来,interface应该是一个好东西,似乎是面向接口编程所必不可少的,interface不就是接口的英文词么。那这篇文章的题目又是怎么回事呢?

还是拿例子说话。
interface Computer{
    function add($a, $b);
    function minus($a, $b);
….
}

class Mainframe implements Computer{
    function add($a, $b){
        //…
    }

    function minus($a, $b){
        //…
    }
}

class Calculator implements Computer{
    function add($a, $b){
        //…
    }

    function minus($a, $b){
        //…
    }
}

不管是大型机还是小计算器,都可以算加减法。想要算加法的时候只要拿一个实现了Computer接口的东西就行。比如
function doAdd(Computer $c, $a, $b){
    return $c->add($a, $b);
}
看起来不错。不过能算加减法的不仅仅是那些用电的东西。我国古代劳动人民的智慧结晶——算盘也能算加法。那么如果要拿算盘算加法,就要有一个算盘类。那这个算盘类该不该实现Computer接口呢?这个算盘似乎也算不上是一个计算机啊。或许该另外建立一个Calculatable的接口,让计算机和算盘都去实现这个接口,这样似乎就不错了。有时候我们只需要用到Calculatable的一部分功能。比如Calculatable定义了基本的加减乘除,但我们买菜算账的时候通常只需要加法。这时候如果用Calculatable的话就需要接受一些无用的东西。再用一个php的SPL里的例子。SPL里有一个ArrayAccess接口,可以让对象像数组一样用下标去访问。这是一个很有意思的语法糖。不过有时候只需要按下标读功能就可以了,但是为了要满足规定,还是要把ArrayAccess接口的所有方法都实现一遍,于是就要写好几个throw new Exception(‘xxx is not writable’);之类的东西。这实在有点罗嗦。

为什么不换一个写法呢?扔掉那些interface,直接写
class Mainframe{
    function add($a, $b){
        //…
    }

    function minus($a, $b){
        //…
    }
}

class Calculator{
    function add($a, $b){
        //…
    }

    function minus($a, $b){
        //…
    }
}

class Abacus{
    function add($a, $b){
        //…
    }

    function minus($a, $b){
        //…
    }
}
在需要计算的时候,也不用限定一定要是Computer,把对象直接扔过去。
function doAdd($c, $a, $b){
    return $c->add($a, $b);
}

如果在试图在一个不能算加法的东西比如易拉罐上执行add,解释器自然会报错。也就是说,只有满足接口的对象才能够正常运行。你看,不用interface一样可以实现面向接口编程,而且更加灵活。

不用interface搞面向接口编程这听起来还是有点玄乎。毕竟这个interface就是接口的英文单词啊。好吧,interface确实是英文单词,但并不代表interface这个语法元素就等价于接口。就好像把狗的尾巴也叫做腿,这狗也不是就有了五条腿。interface只是叫了这个名字而已。当然,interface在一定程度上也为面向接口编程提供了方便,比如提供了编译期的检查。而且对于强类型语言来说,这样的语法元素是很重要的,比如java。而对于弱类型语言,比如php,却不是必须的。而对于一些更动态的语言,比如python,ruby来说,更是很难相容的。

 

Lucene学习笔记

  最近用Lucene开发全文检索。《Lucene in Action》这本书用的是Lucene 1.4。我自己下的是最新的2.1。然后就发现了很多不同的地方。

 Field没了Keyword、UnIndexed、UnStored、Text这几个静态成员,只能用
Field(String, String, Store, Index)。
Keyword对应Field.Store.YES, Field.Index.UN_TOKENIZED,
UnIndexed 对应Field.Store.YES, Field.Index.NO,
UnStored对应Field.Store.NO, Field.Index.TOKENIZED,
Text对应Field.Store.YES, Field.Index.TOKENIZED。

FSDirectory.getDirectory的有两个参数的变成了depresed 了。现在要用只有一个参数的。

BooleanQuery的add方法也变了。原来是用两个boolean值组合的,现在 使用BooleanClause.Occur的几个静态成员了。

暂时就发现这点差异。

 

Java又神奇了一次

今天写程序,碰到了这么一个错误:
java.sql.SQLException: Column Index out of range, 0 > 1.
前面好理解,后面那个就神奇了: 0 > 1
其实是这样的。JDBC的RecordSet的数字列索引是从1开始的。我给了个0,所以就超出了范围。不过估计写这个错误信息的高人一定没犯过我这样的错误吧。

Java vs. PHP

  最近在做一个J2EE的项目,用Spring + Hibernate。在开发过程中,我体会到了Java语言的麻烦,在许多动态语言中可以很简单的解决的问题在Java中就相当麻烦。但Java作为一种强类型语言的好处就在于方便了IDE。而对动态语言很难实现功能强大的辅助开发功能。不过对于开发者,尤其是熟练的开发者来说,IDE带来的帮助远不如语言本身的灵活性带来的好处大。如此看来,RoR的出现对Java社群带来的影响就是理所当然的了。
  相比起Java,PHP要简单得多。而且便于快速开发快速测试。在J2EE里,要对Servlet或者Controller/Action之类的东西做单元测试是比较麻烦的。因为不能自己构造request和response。虽然Spring提供了Mock对象,也有其他人做了类似的工作,相比 PHP里只要设置下$_GET,$_POST就能自定义请求数据来说,就要复杂了很多。
  Spring号称是一个轻量级框架,但实际用下来还是比较麻烦的。可以想见,传统的J2EE开发就更痛苦了。对于不那么复杂的Web应用,也是多数Web应用,其实还是PHP更适合。
  但PHP并没有受到很大的重视,一直都是叫座不叫好型的。PHP本身也有不少缺点,比如没有namespace/package,没有一个被普遍接受的好的应用框架,没有应用服务器。但更重要的原因恐怕还是PHPer普遍对面向对象认识不足。虽然PHP提供面向对象功能已经很久了,但多数程序里还是把 PHP当作面向过程的语言来用,在一些高人的眼里,PHP就成了低端语言。

  

expected: <男> but was: <男>

  今天写程序,突然看到这个错误:junit.framework.AssertionFailedError: expected:<男> but was:<男>
把错误发到群里,震惊了世界后,突然发现了原因。
  assertEquals(resume.getSex(), “男”);
应该是
  assertEquals(resume.getSex().toString(), “男”);
  少了个toString()。但是我自己为了前台输出方便,在Sex类里实现了toString。结果junit在输出错误的时候就输出了那个”男”,然后就有了那个神奇的错误信息。