java 的 static binding

无意间看见了一篇关于 logging 实现的文章,里面比较了 slf4j 和 jakarta common logging(jcl)两个 facade 之间的区别,稍微提了一下,前者使用的 static binding 而后者使用的是 dynamic binding,因此某些情况下后者可能会出现问题。在 slf4j 的介绍里面提到,

… SLF4J is conceptually very similar to JCL. As such, it can be thought of as yet another logging facade … SLF4J static binding approach is very simple, perhaps even laughably so. It was not easy to convince developers of the validity of that approach …

感觉这里就是说咱其实也没啥牛逼的,但是爷说了大家都不信,只好自己写了个证明给大家看。那么 jcl 和 slf4j 到底如何进行的 binding?

用过 log4j 的同志大致知道,一般都会通过一个 factory 来构造 logger 放在 private static final 的 log 变量里面,之后就用这个来写 log 了,如果你还玩过别的 logging 实现的话,大家似乎都有那么一点点类似的地方,为了实现一个简单的 facade,很自然的抽象就是一个 factory 一个 logger,然后嘛别的实现都向这个接口 adapt 一下,load 的时候选择一个实现类弄上去。

都说 jcl 简单,先来看看它,

src/main/java/
├── org
│   └── apache
│       └── commons
│           └── logging
│               ├── impl
│               │   ├── AvalonLogger.java
│               │   ├── Jdk13LumberjackLogger.java
│               │   ├── Jdk14Logger.java
│               │   ├── Log4JLogger.java
│               │   ├── LogFactoryImpl.java
│               │   ├── LogKitLogger.java
│               │   ├── NoOpLog.java
│               │   ├── package.html
│               │   ├── ServletContextCleaner.java
│               │   ├── SimpleLog.java
│               │   └── WeakHashtable.java
│               ├── LogConfigurationException.java
│               ├── LogFactory.java
│               ├── Log.java
│               ├── LogSource.java
│               └── package.html
└── overview.html

从这个结构上来看 impl 里面的类对应的是一种 logger 的实现,编译这个玩意看起来得小心一点,可能你会说我大不了把这个 package 分拆成 api 和具体实现的 package,但是其实这样也不解决后面谈到的问题(参看这篇文章)。为什么这么说?可以考虑这样一件事情,如果 classpath 里面存在若干 logging 实现,如 JDK 的 logging interface 和 log4j,jcl 肯定需要某种策略选择使用哪个 binding,从这个角度来说 binding 的确是 runtime 选的。这部分逻辑可以参看 LogFactory.java 的实现,它利用了 TCCL(thread contextual class loader)来寻找需要的实现。

public static LogFactory getFactory() throws LogConfigurationException {
    ClassLoader contextClassLoader = getContextClassLoaderInternal();
    // ...
    LogFactory factory = getCachedFactory(contextClassLoader);
    // ...
}

这大致的意思就是说 LogFactory 的构造是通过 ClassLoader 加载对应配置的实现类来做到的,因此如果某个 binding 不在 classpath 里面,ClassLoader 就会报错,这样就能回退到某些默认的实现上了,看起来似乎逻辑简单明了。但是很遗憾有个弱点,那就是 java 的 ClassLoader 的实现作了比较强的假定。

实际上 Java 为了某些应用的需要 ClassLoader 是有层次关系的,比如 Tomcat 可以加载用户的 war,每个 war 其实都是用 child class loader 来加载进去的(父 class loader 为 Tomcat 本身加载类)。child class loader 一般 load 的逻辑都是 parent first search,这是什么意思呢,就是 load 类的话先问 parent 要,parent 找不到的话自己再找。当然像 Tomcat 这类 container 它创建的 child class loader 就不应该先问 parent 要了,每个 webapp 的 class loader 应该找自己要 Class,这种一般称为 child first search。

实际上造成 dynamic binding 问题的正是这种 class loader delegation 的问题。试想我们有一个类 Foo 它的 bar 方法通过 jcl 写 log,我们将其 API 称为 IFoo

  • 如果系统 class loader 能看见 jcl、IFoo,PFS 的 child class loader 能看见 Foo、jcl 和 log4j,我们通过后者加载 Foo 调用 bar 却发现并没有使用 log4j 而是在用 java 的 logging API,这是因为 jcl 的 TCCL 使用了系统 class loader,它只能看见 jcl 并没有实现的 jar,最后 by default 使用了 java logging API
  • 类似以上 setup,但是我们在通过 child class loader 初始化 Foo 之前,将 TCCL 设置为该 child class loader,这时 Bar 为了输出 log 问 LogFactory 要 logger,LogFactory 通过 TCCL 一看,哦找到 log4j 了,于是就要 load 对应 log4j 的 binding 了,这是 PFS,所以系统 loader 被要求加载这个类,但是这个 loader 看不见 log4j 的实现类,所以最后 ClassNotFound 了
  • 你可能说上面的 case 是因为 binding 在系统 loader 可见的地方,但是实现类没有在那里,我们可以把 API 和 binding 分开到两个 jar 里面,上面的只有 API,下面能看见 binding 和实现,这样总没问题了吧?的确这样能 work 了,但是会产生新的 limitation(为什么?因为 parent 也许也需要使用 jcl 的实现)
  • 假使我们换 CFS 的 child class loader(与 Tomcat 类似),若设置 TCCL 为 child class loader,这时 parent class loader 不能加载 jcl,因为 child class loader 已经看到了 jcl,不同的 class loader 获得的类是“不兼容的”(Java 可以通过 class loader 改变类,比如 load time weaving,因此不能假定另一个不同于系统的 loader 获得的 jcl 是一样的,前面的例子 child class loader 与 system class loader 一致)
  • 假使我们不直接用 parent class loader 加载 jcl,而是通过 child class loader 加载 Foo,如果我们不包括 log4j,此时里面的 bar 使用的 jcl 会使用 java logging api
  • 类似前面,如果 child class loader 能看见 log4j,不能看见 jcl,这样 TCCL 能看见 log4j,但是 binding 在 parent class loader 里面进行,找不到 log4j 的实现

所以不论怎么配置都会产生一系列的问题,特别是如果在 tomcat 里面干活的话,为此,tomcat 将 jcl 私自定制了一份,避免以上的问题

Since Tomcat 6.0, Tomcat uses a private package-renamed implementation of Apache Commons Logging, to allow web applications to use their own independent copies of the original Apache Commons Logging library. In the default distribution this private copy of the library is simplified and hardcoded to use the java.util.logging framework.

现在为什么 slf4j 可以避免这个问题呢?slf4j 的结构稍微有点不同,提供了 api 和实现 binding package,我们看看它怎么处理初始化 binding 的。

public final class LoggerFactory {
  // ...
  private final static void bind() {
    try {
      Set staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
      reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
      // the next line does the binding
      StaticLoggerBinder.getSingleton();
      INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      reportActualBinding(staticLoggerBinderPathSet);
      emitSubstituteLoggerWarning();
    }
    // ....
  }
  // ...
  private static Set findPossibleStaticLoggerBinderPathSet() {
    // use Set instead of list in order to deal with  bug #138
    // LinkedHashSet appropriate here because it preserves insertion order during iteration
    Set staticLoggerBinderPathSet = new LinkedHashSet();
    try {
      ClassLoader loggerFactoryClassLoader = LoggerFactory.class
              .getClassLoader();
      Enumeration paths;
      if (loggerFactoryClassLoader == null) {
        paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
      } else {
        paths = loggerFactoryClassLoader
                .getResources(STATIC_LOGGER_BINDER_PATH);
      }
      while (paths.hasMoreElements()) {
        URL path = (URL) paths.nextElement();
        staticLoggerBinderPathSet.add(path);
      }
    } catch (IOException ioe) {
      Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
  }
  // ...

这个基本意思就是数数了,看看有几个叫 StaticLoggerBinder 的类(来自每个 binding 的 jar),选择一个加载,这不依赖于外部的配置,完全根据 load 时的 binder 选择实现类,因此以上情况下不会因为层次 class loader 加载了意料之外的实现或者崩溃。

JCL’s discovery mechanism invents new and original ways of shooting yourself in the foot.

珍爱生命,远离 jcl。

——————
And Rachel said, God hath judged me, and hath also heard my voice, and hath given me a son: therefore called she his name Dan.

Advertisements
java 的 static binding

一个有关“java 的 static binding”的想法

  1. guojc 说:

    java 只有 dynamic linking才是一切祸首, static linking多好啊 go就选择默认 static linking. pack jar时被各种相同lib不同版本折磨的路过。 load class应该支持指定版本或 md5啊…

    1. guojc 说:

      因为jvm各种不给力啊,最大的抱怨jvm内存效率低下,一个有两个float field的array of structure,jvm下要4倍的内存啊。内存不给力也就算了,你说你搞分布式设计来堆机器,但是gc效率也很差。60GB的堆,回收一次要stop world,几十秒到几分钟啊。其他peer会认为你挂了。总体来说,照我个人经验来看,像spark这类in-memory distributed compute framework现在是完全被jvm给坑了。。。

      不过go也似乎没彻底解决问题,明明是冲着server系统来的。用的gc 算法居然是conservative stop world gc,还没到jvm的generational parallel gc的程度啊,真搞不懂 google怎么想的。

      你有没有比较好的jvm gc调优经验啊?你们怎么处理gc引起的response time 不达标问题的?

  2. zt 说:

    没玩过 gc 啊,你上次不是说有个 Azul 的 GC 么?那个不是说不会 stop the world 么 ?

    我一直想要是有两个 heap 给 GC 用,一个碎片化了就 copy 到新的内存里面去,copy 几个算几个,把新的引用转过去,等到老的 heap 里面的 copy 完了,直接把老的整个释放然后等新的碎片化,反复下去,这样行不行?

    好像 C/C++ 程序抱怨碎片化好一点?不知道是不是可以定制 memory pool 避免异质数据混合存放,内存利用率也高一点?

    1. guojc 说:

      那个zing jvm要付费的,企业级授权没个几千刀一台机器是不可能的。这种我这个level的人不可能搞啊。另外恭喜你刚描述最基本的compact stop wolrd gc策略,问题是gc这个过程在大heap上是分钟级别的。。。。 最近调研了下,一种可能的做法是用unsafe手动分配内存,对内存手动管理。不过unsafe,读要慢10倍,写要慢2倍。值不值票比较难讲。

    1. guojc 说:

      因为要计算survivals, 这时需要write barrier 确保没有新的对象或reference创建, 然后move时,需要read barrier 确保不会读到半成品。最简单的手段就是stop world,如果你把这些事情并行化分对象生存期的话就是generational gc,分memory区段来做那么就是jvm 的 G1算法,然后zing jvm就是利用cpu上 virtual memory lookup table 来做,取代正常操作系统的virtual memory处理程序,从而实现高效read barrier和write barrier,与之对比jvm的read barrier是很弱的。

  3. zt 说:

    你说的前面那些算法好像都没有 incremental 的啊,还是类似 batch 或者 small batch 啊,这种事情 incremental 很难么?

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s