Pull to refresh

Секреты JDK

Reading time 4 min
Views 25K

Про Unsafe в Java не слышал только ленивый, однако это не единственный магический класс в Sun/Oracle JDK, стирающий границы Java платформы и открывающий тропинки, не нанесенные на карту публичного API. Я расскажу про некоторые из них, принесшие пользу в реальных проектах. Но помните: недокументированные возможности лишают ваше приложение переносимости на другие Java платформы и, кроме того, являются потенциальным источником нетривиальных ошибок. Я даже зря написал слово «приложение». Лучше сказать, что описанные ниже классы вовсе не годятся для приложений! Скорее, они представляют интерес лишь для системного ПО и для любознательных программистов, т.е. для вас :)


Cleaner


Каждый хороший Java-программист знает, что финализаторы — зло. Но чем их в таком случае заменить, если нет гарантии, что все, использующие нашу библиотеку, как порядочные программисты, будут вызывать close()? В конце концов, что используется внутри самого JDK?
В системных классах в качестве более надежной и легковесной замены финализаторам применяется sun.misc.Cleaner, основанный на PhantomReference. Как только сборщик мусора обнаружит, что объект, на который ссылается Cleaner, стал phantom-reachable, будет запущен указанный Runnable, как правило, для освобождения ресурсов:
    public NeedsCleanup() {
        this.resource = unsafe.allocateMemory(16*1024*1024);
        Cleaner.create(this, new Destructor(resource));
    }

    private static class Destructor implements Runnable {
        final long resource;

        Destructor(long resource) {
            this.resource = resource;
        }

        public void run() {
            unsafe.freeMemory(resource);
        }
    }


Несмотря на свои полезные стороны, этому классу не суждено стать частью публичного API, поскольку при неумелом использовании он может заблокировать поток Reference Handler, а то и вовсе остановить приложение.

Unmapping MappedByteBuffer


А вот как раз и пример Cleaner'а в системных классах JDK.
Кто пользовался в Java memory-mapped файлами посредством FileChannel.map(), вероятно, не раз посожалел, что у MappedByteBuffer'а нет метода unmap(). На то есть свои причины.
Но, как в поговорке, если нельзя, но очень хочется — то можно. Реализация MappedByteBuffer создает Cleaner для выполнения unmap после сборки мусора, когда буфер становится недостижим. Так вот, этот Cleaner можно вызывать и вручную:
    public static void unmap(MappedByteBuffer bb) {
        if (bb instanceof sun.nio.ch.DirectBuffer) {
            ((sun.nio.ch.DirectBuffer) bb).cleaner().clean();
        }
    }


SharedSecrets


Интригующее название, не правда ли? sun.misc.SharedSecrets — утилитный класс, предоставляющий интерфейсы доступа к некторым приватным данным системных классов. Хотя интересного там не так много. Можно, скажем, добраться до constant pool любого загруженного класса… только не спрашивайте, зачем :)
Из того, что довелось использовать мне — получение нативного (OS-level) дескриптора файла из Java-объекта FileDescriptor. Делается это так:
    public static int getNativeFD(FileDescriptor fd) {
        return SharedSecrets.getJavaIOFileDescriptorAccess().get(fd);
    }

Или вот еще, для примера, как узнать кодировку консоли, из которой запущено Java-приложение:
    if (System.console() != null) {
        System.out.println(SharedSecrets.getJavaIOAccess().charset());
    }


SignalHandler


А знаете ли вы, что в Java программе можно ловить и обрабатывать POSIX сигналы, например, SIGINT?
Да-да, с помощью классов sun.misc.Signal и sun.misc.SignalHandler.
Впрочем, на этом я останавливаться не буду, поскольку статья на эту тему уже была.

MagicAccessor


А самым магическим классом, как уже видно из названия, сегодня будет sun.reflect.MagicAccessorImpl. Будучи унаследованным от него, ваш класс чудесным образом получит возможность обращаться ко всем приватным полям и методам любых классов! Фокус в том, что верификатор байткода попросту опускает проверки доступа для классов-наследников MagicAccessorImpl.
Мне он очень пригодился для реализации собственного быстрого механизма сериализации. Ведь сериализатор должен уметь читать и писать любые, в том числе и приватные, поля классов. Традиционно это достигается посредством Reflection, что довольно медленно, либо с помощью Unsafe. Но нет способа быстрее, чем обращаться к полям напрямую через байткоды getfield/putfield, которые компилируются буквально в одну машинную инструкцию.

// ----- A.java -----
class A {
    private int privateField = 5;
}

// ----- B.java -----
class B extends sun.reflect.MagicAccessorBridge {
    public static void main(String[] args) {
        A a = new A();
        a.privateField = 10;
        System.out.println(a.privateField);
    }
}

// ----- MagicAccessorBridge.java -----
package sun.reflect;

public class MagicAccessorBridge extends MagicAccessorImpl {
    // Since MagicAccessorImpl is package-private, we'll make a public bridge
}

Одна лишь проблема: про магический аксессор знает лишь JVM, но не javac, который настойчиво продолжит ругаться на несанкционированный доступ. Чтобы проверить пример, вам придется сначала поменять в классе A модификатор поля на public, все скомпилировать, затем вернуть обратно модификатор private и перекомпилировать класс A отдельно. Разумеется, неудобно, поэтому на практике наследоваться от MagicAccessorImpl имеет смысл только при динамической генерации класса. Кстати говоря, в сокровищнице JDK найдутся инструменты и на этот случай: sun.tools.asm.Assembler и целиком пакет sun.tools.asm.

Warning: Sun proprietary API


Как только вы попробуете разбавить свою программу одним из вышеописанных классов или же любым другим из пакета sun.*, непременно получите от компилятора предупреждение
warning: sun.misc.Unsafe is Sun proprietary API and may be removed in a future release

которое никакими аннотациями подавить не удастся, и поделом!

Можно радоваться или огорчаться, но в JDK 7, наконец, появилась возможность скрыть назойливый warning. Однако, опять же, только с помощью секретного ключика :)

Дописываете к методу или классу аннотацию
@SuppressWarnings("sunapi")

и запускаете javac с параметром
javac -XDenableSunApiLintControl TestUnsafe.java

Вот и все, путь к экспериментам свободен!
Tags:
Hubs:
+125
Comments 30
Comments Comments 30

Articles