Секреты JDK


    Про 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

    Вот и все, путь к экспериментам свободен!
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 30

      +40
      Ты только что разрушил мой маленький безопасный мир Явы!
        +5
        А может быть наоборот, он показал где стоит искать магию в чужом приложении, когда наблюдаемые баги начинают выходить за грани разумного и ты уже не понимаешь, где еще смотреть косяки?
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Ага, интересная штука. Хотя применять не доводилось. А тебе? И внутри JDK, похоже, нигде не используется.
          • НЛО прилетело и опубликовало эту надпись здесь
            • НЛО прилетело и опубликовало эту надпись здесь
                –1
                «Мы с Doug Lee...» звучит очень круто. Что, прямо на самом деле?
                • НЛО прилетело и опубликовало эту надпись здесь
            –2
            Я чего-то не понял. Почему команду интересует безопасность межпоточной работы MappedByteBuffer, при том, что у них есть класс, наследование от которого даёт доступ ко всем приватным членам?
              +2
              MappedByteBuffer — часть public API Java. Его использование документировано и поощряется. MagicAccessor таковым не является.
                0
                Да, но если им может воспользоваться каждый, кто о нём знает, не является ли это проблемой безопасности?
                  +1
                  Гм. Нет, конечно. Причем тут безопасность? Каждый знает, что Яву можно пропатчить или посмотреть дебаггером. Класс отмечен как внутренний. Хотите — пользуйтесь, только не говорите потом, что команда Oracle виновата в том, что софт на вашем кластере за миллион долларов глюканул из-за неумелого использования вами недокументированных и даже явно отмеченных как внутренние классов Java.

                  А вот если MappedByteBuffer начнет работать некорректно при документированном, легитимном использовании, команда Oracle будет в этом виновата. Я уж не знаю про материальную ответственность, но с моральной точки зрения, в глазах сообщества — несомненно.
                    0
                    Я вообще вот к чему. Положим есть система запуска заданий от различных пользователей, полагающаяся на систему безопасности Java. Система запуска читает из файла задания путь к jar файлу, создаёт новый поток, переходит в нём в контекст безопасности пользователя Alice, автора этого задания. Затем выполняет аналогичные действия с заданием Bob'а. Далее задание Alice открывает MappedByteBuffer.

                    Теперь внимание: в комментарии к багу написано, что размапить этот буфер вручную нельзя, т.к. тогда если сразу после этого Bob замапит другой буфер на освободившуюся память, то Alice, из-за того, что разработчки Java хотят быстрый доступ к этим буферам и потому не хотят при каждом обращении проверять, был ли он освобождён или нет, сможет через ссылку на класс размапленного буфера получить доступ к файлам Bob.

                    Но! Если Alice знает про MagicAccessor и наследует он него свой класс, то она может напрямую получить доступ к приватным данным Боба, содержащихся в приватных полях класса, ссылку на который он ей как-нибудь передал. Или, хуже того, через приватные поля классов самой JVM или какой-нибудь из совместно используемых библиотек.
                      +2
                      Если Alice знает про MagicAccessor и наследует он него свой класс...

                      Но ведь с таким же успехом Alice может использовать JNI в своем классе / jar'е и делать вообще все, что заблагорассудится. Ну или просто приватные поля можно через Reflection читать. Но при желании подобные вещи можно отловить верификатором. А вот некорректное использование функционала «размапливания» буфера будет не отловить. Поскольку сама операция будет легитимной. Опасны последствия.
                        +5
                        В Java есть понятия политики безопасности и пермиссий. Когда вы запускаете java локально, SecurityManager выключен, поэтому вам и позволены все эти хаки. Теперь попробуйте запустить java с параметром -Djava.security.manager и тут же получите java.security.AccessControlException: access denied (java.lang.RuntimePermission accessClassInPackage.sun.misc). Java — безопасная платформа (для аплетов, например, SecurityManager всегда включен), но она не ограничивает возможности тех, кто знает, что делает.
                +3
                ДА!

                You made my day. Спасибо за этот пост!
                  0
                  Спасибо, уволок в избранное. Может пригодиться при отладке чужого кода.
                    0
                    Не могу понять чем использование sun.misc.Cleaner лучше «Finalizer Guardian idiom»?
                      +2
                      Отвечу комментариями к исходникам:
                      Cleaners are a lightweight and more robust alternative to finalization. They are lightweight because they are not created by the VM and thus do not require a JNI upcall to be created, and because their cleanup code is invoked directly by the reference-handler thread rather than by the finalizer thread. They are more robust because they use phantom references, the weakest type of reference object, thereby avoiding the nasty ordering problems inherent to finalization.

                      Cleaners may also be invoked directly; they are thread safe and ensure that they run their thunks at most once.
                      0
                      Также рекомендую к прочтению:
                      0
                      Очень интересно и познавательно! Огромное спасибо!
                        0
                        О, неужто теперь в университетах появится (блекджек и шлюхи) порно?

                        А если серьезно, то домены можно было бы использовать и по назначению, т.е. и сексуального образования. А то абстрактным предметам, которые, возможно, никогда и не пригодятся, в университетах обучают, а тому, с чем каждый имеет дело каждый день — нет. В результате имеем лицемерие: с одной стороны тема заглатывается, с другой — пороки легкодоступны, и не только касаемо секса.

                        А еще серьезней — получается, каждый должен купить домен в зоне .ru, .com и .xxx, «чтобы какой-нибудь негодяй не опозорил мое имя и не обманул мою потенциальную аудиторию!»?
                          0
                          айй, не туда…
                            +1
                            Вы вынесли мне моск, абсолютно! Перечитал два раза, пытасяь понять, как ваш комментарий относится к топику)
                          0
                          А сюда вот что скажу — спасибо, интересно, только жаль, что эти методы не надежны (могут не быть или исчезнуть) и не становятся частью public API, а было бы весьма полезно…
                            +1
                            Инетересно. Еще встречал в проектах
                            sun.misc.BASE64Encoder
                            

                              +1
                              Один я не могу найти в JDK 6 класс sun.misc.MagicAccessorImpl?
                              Возможно вы имели ввиду sun.reflect.MagicAccessorImpl?
                                0
                                Конечно, sun.reflect. В примере указал правильно, а здесь на автомате не то написал. Спасибо, поправил.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое