Java9でsun.misc.Cleanerのpackageが移動してしまうことへの対処(Apache Hadoopの場合)


## 概要

JDK9では、[JDK-8148117](https://bugs.openjdk.java.net/browse/JDK-8148117)などによって、`sun.misc.Cleaner`が`jdf.internal.ref.Cleaner`に移動してしまった。JDK9だけで動かす前提なら単純にimport文を書き換えれば良いが、JDK9とJDK8の両方でビルドを通したい場合は黒魔術的な対処が必要になる。以下、この黒魔術について解説する。

## 目的

[[HADOOP-12760]sun.misc.Cleaner has moved to a new location in OpenJDK 9](https://issues.apache.org/jira/browse/HADOOP-12760)をなんとかしたい

## 詳細

問題となっているコード

```java:NativeIO.java
    /**
     * Unmaps the block from memory. See munmap(2).
     *
     * There isn't any portable way to unmap a memory region in Java.
     * So we use the sun.nio method here.
     * Note that unmapping a memory region could cause crashes if code
     * continues to reference the unmapped code.  However, if we don't
     * manually unmap the memory, we are dependent on the finalizer to
     * do it, and we have no idea when the finalizer will run.
     *
     * @param buffer    The buffer to unmap.
     */
    public static void munmap(MappedByteBuffer buffer) {
      if (buffer instanceof sun.nio.ch.DirectBuffer) {
        sun.misc.Cleaner cleaner =
            ((sun.nio.ch.DirectBuffer)buffer).cleaner();
        cleaner.clean();
      }
    }
```

どうでもいいコメント

* mlockはCのコードを呼び出しているのにmunmapがそうしていないのは謎

  * 過去の経緯([HDFS-4953](https://issues.apache.org/jira/browse/HDFS-4953?focusedCommentId=13763364&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13763364))を読むと、JNIへの依存を避けてSun-specific APIを使うことにしたようだが、他のコードはガンガンJNIを使っているので、一貫性がない
  * (追記) munmapはWindowsで動作させられないので、Javaで書いている

JDK9では`sun.misc.Cleaner`が移動してしまっているので、このコードは動かない。そのため、以下のようなコードで対応する。

```java:
  private static MethodHandle cleanMethod = null;
  static {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    try {
      final Class<?> directBufferClass =
          Class.forName("java.nio.DirectByteBuffer");
      final Method m = directBufferClass.getMethod("cleaner");
      // Set permission to the lookup class for making
      // a direct method handle
      m.setAccessible(true);
      // Make a direct method handle
      MethodHandle directbufferCleanerMethod = lookup.unreflect(m);
      // Get the return type of java.nio.DirectByteBuffer
      // JDK8: sun.misc.Cleaner
      // JDK9: jdk.internal.ref.Cleaner
      Class<?> cleanerClass = directbufferCleanerMethod.type().returnType();
      // Get the method (Cleaner#clean)
      cleanMethod = lookup.findVirtual(
          cleanerClass, "clean", methodType(void.class));
    } catch (NoSuchMethodException | IllegalAccessException |
             ClassNotFoundException ignored) {
    }
  }
```

http://hg.openjdk.java.net/jdk9/jdk9/jdk/rev/07dcdd5d9401 によると、`java.nio.DirectByteBuffer#cleaner`の戻り値が

* JDK8: `sun.misc.Cleaner`
* JDK9: `jdk.internal.ref.Cleaner`

になっているので、これを呼び出している。munmapメソッドは以下のように書き換えられる。

```java:
     public static void munmap(MappedByteBuffer buffer) {
       if (buffer instanceof sun.nio.ch.DirectBuffer) {
         if (cleanMethod != null) {
           try {
             // Execute Cleaner#clean
             cleanMethod.invokeExact(
                 ((sun.nio.ch.DirectBuffer)buffer).cleaner());
           } catch (Throwable ignored) {
           }
         }
```

`Class.forName`で`sun.misc.Cleaner`もしくは`jdk.internal.ref.Cleaner`を直接呼び出してもいいのでは、という案もあるが、`jdk.internal.ref.Cleaner`がいつまで居座っているかもわからないのでこういった手法をとっている。

* 参考: [JDK-8149925: We don't need jdk.internal.ref.Cleaner any more - part 1](https://bugs.openjdk.java.net/browse/JDK-8149925)


## 参考

同様の手法(黒魔術)は、Apache Luceneで既に実施されており、本記事はこれに基づいたものである。

* Apache Luceneにおける対処: [MMapDirectory.java (GitHub)](https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/store/MMapDirectory.java#L339)