1. Класс sun.misc.Unsafe
2. Структуры виртуальной машины
3. Особенности версии 1.5
4. Применение на практике
5. Бесконечный final
Как известно при разработке языка Java с самого начала делался упор на
"безопасность" кода (так называемый "safe code"). Помимо всего прочего
это означало отказ от указателей, работы с памятью и тому подобных
низкоуровневых средств. Совсем отказаться правда не удалось, пришлось
оставить лазейку, в первую очередь естественно для собственных
классов. Но все, что использует Java Runtime, можем использовать и мы.
В этой статье мы научимся писать небезопасный код на Яве и используем
новоприобретенные знания для решения некоторых интересных проблем,
которые штатными средствами Явы не решаются.
Рассматривать мы будем только Sun'овскую виртуальную машину, по двум
причинам. Во-первых она применяется наиболее широко и является
своеобразным эталоном. Во-вторых до IBM'овской у меня руки еще не
дошли, а больше никаких (реально использующихся) я не знаю. Все
приведенные ниже примеры протестированы с Java 1.4.2_11 и 1.5.0_06.
Предполагается что читатель достаточно хорошо разбирается как в Яве,
так и в общих принципах программирования.
1. Класс sun.misc.Unsafe
Малоизвестный класс sun.misc.Unsafe входит в комплект Sun Java Runtime
начиная с первых версий. Как и все остальные классы в package sun.*,
Unsafe не документирован, но имена (в большинстве своем нативных)
функций, видимые при декомпиляции, говорят сами за себя. Явно
присутствуют функции работы с памятью (allocateMemory,
freeMemory,...), чтения и записи значений по заданному адресу(putLong,
getLong,...) и некоторые более специализированные(throwException,
monitorEnter,...). То есть в принципе все, что нам нужно.
Правда так просто инстанциировать Unsafe не удастся. Единственный
constructor - приватный, а в getUnsafe() проверяется загрузчик
вызвавшего класса и объект возвращается только если класс загружен
Bootloader'ом. В противном случае получаем SecurityException.
public static Unsafe getUnsafe()
{
Class class1 = Reflection.getCallerClass(2);
if(class1.getClassLoader() != null)
throw new SecurityException("Unsafe");
else
return theUnsafe;
}
К счастью существует еще внутренняя переменная theUnsafe, до которой
мы можем добраться с помощью Reflection. Всю черновую работу соберем в
один класс (назовем его UnsafeUtil), который будем расширять по мере
надобности.
public class UnsafeUtil {
public static Unsafe unsafe;
private static long fieldOffset;
private static UnsafeUtil instance = new UnsafeUtil();
private Object obj;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Конечно можно просто внести UnsafeUtil в список загружаемых
Bootloader'ом классов (указав путь в ключе -Xbootclasspath/a) и
вызывать getUnsafe() в соответствии с замыслом Sun. Беда в том, что
тогда все использующие UnsafeUtil классы также должны быть прописаны в
bootclasspath'е (см. главу "5.3 Creation and Loading" в VM spec).
Правда например package java.nio как-то ухитряется обходить это
ограничение, но как именно пока не очень понятно. К тому же этот
способ выходит за рамки "чистого" кода, так как требует дополнительных
стартовых опций для виртуальной машины. Так что не будем мудрствовать
и ограничимся чтением theUnsafe.
В первую очередь нам понадобятся естественно операции референцирования
и дереференцирования, ObjectToAddress и AddressToObject
соответственно.
public static long ObjectToAddress (Object o){
instance.obj = o;
return unsafe.getLong(instance, fieldOffset);
}
С ними мы уже достаточно хорошо вооружены в техническом плане, не
хватает только информации по внутреннему устройству Явы. Ее мы найдем
в следующем разделе.
Очень похожую реализацию кстати сделал Don Schwarz
(http://don.schwarz.name/index.php?p=30). Это одно из очень немногих
мест, где можно найти хоть какие-то примеры работы с классом Unsafe. К
сожалению Don в свое время не оценил потенциал низкоуровнего
программирования в Яве и остановился, сделав всего пару робких шагов.
Мы же пойдем дальше.
2. Структуры виртуальной машины
Теперь посмотрим, в каком виде виртуальная машина (версии 1.4) хранит
данные в памяти. Поскольку Ява работает с классами и их инстанциями,
то ими и займемся.
Инстанция:
instance_struct {
0 magic // всегда равен 1
4 class // указатель на структуру класса, class_struct*
8 ... // Дальше идут подряд переменные инстанции(то есть все которые не static),
12 ... // порядок пока не очень понятен, судя по всему в порядке объявления и
16 ... // обьекты перед примитивными типами
...
}
Переменные типа double и long занимают 64 бита, остальные по 32. В
памяти инстанции выравниваются по 64-битной границе, дополняются при
необходимости нулями. То есть по сути мы имеем обыкновенную сишную
структуру плюс указатель на ее описание.
Класс:
class_struct {
0 magic // всегда равен 1
4 class // class_struct*, указатель на структуру класса более высокого уровня, зачем нужен - непонятно
8 ... // значение неизвестно
12 super_count // количество уровней наследования: 0x18 - один (наследует от Object),
// 0х1c - два и т.д. до восьми(0х34), потом 0х10. У интерфейсов тоже 0х10.
16 interface // class_struct*, указатель на какой-либо из интерфейсов класса (на какой именно непонятно), часто просто 0
20 interface_list // указатель на массив с элементами типа class_struct*, все интерфейсы класса
24 ...
28 ...
32 ...
36 ... // указатели на структуры восьми высших суперклассов начиная от Object и кончая this (если поместится)
40 ...
44 ...
48 ...
52 ...
56 size // размер инстанции класса в DWORD'ах
60 this_class // instance_struct*, указатель на инстанцию java.lang.Class соответствующую данному классу
64 access_flags // доступ к классу как описано в VM spec ( 0х1 - public, 0х10 - final и т.д.)
68 ...
...
}
Здесть я привел только те куски, которые мы будем использовать в
дальнейшем и в которых я более или менее уверен. На самом деле
class_struct значительно длиннее и содержит кроме того указатели на
функции класса, статические переменные и все остальное, что может
понадобится виртуальной машине. Все эти структуры по понятным причинам
нигде не документированы и разбираться надо вручную - хоть и несложно,
но достаточно трудоемко. Если у кого-то есть желание помочь, буду
только рад.
3. Особенности версии 1.5
С переходом на последнюю (на момент написания) версию 1.5.0_06
внутренние структуры виртуальной машины претерпели некоторые
изменения. К счастью небольшие: изменился в основном порядок полей,
значения остались в большинстве прежними. Структура класса выглядит
теперь следующим образом:
class_struct_1_5 {
0 magic // всегда равен 1
4 class // class_struct*, указатель на структуру класса более высокого уровня, зачем нужен - непонятно
8 ... // значение неизвестно
12 size // размер инстанции класса в DWORD'ах
16 super_count // количество уровней наследования: 0x20 - один (наследует от Object),
// 0х24 - два и т.д. до восьми(0х38), потом 0х14. У интерфейсов тоже 0х14.
20 interface // class_struct*, указатель на какой-либо из интерфейсов класса (на какой именно непонятно), часто просто 0
24 interface_list // указатель на массив с элементами типа class_struct*, все интерфейсы класса
28 ...
32 ...
36 ...
40 ... // указатели на структуры восьми высших суперклассов начиная от Object и кончая this (если поместится)
44 ...
48 ...
52 ...
56 ...
60 this_class // instance_struct*, указатель на инстанцию java.lang.Class соответствующую данному классу
64 ...
68 ... // точные значения неизвестны
72 ...
76 ...
80 access_flags // доступ к классу как описано в VM spec ( 0х1 - public, 0х10 - final и т.д.)
84 ...
...
}
Обратите внимание на переехавшее вперед поле size и сдвинутые по
сравнению с версией 1.4 значения поля super_count. При написании кода
придется учитывать подобные мелкие отличия. Поэтому будем в самом
начале опрашивать версию виртуальной машины и сохранять результат в
переменной vm1_5. Интересно кстати, что версии 1.5.0_0x, x<6
используют все еще старые структуры. То есть достаточно глобальные
изменения в виртуальной машине не обязательно приурочены к
значительному скачку версии - сам по себе примечательный факт.
4. Применение на практике
Перейдем к практической части и попробуем приспособить теорию к делу.
Для начала решим одну проблему, которая существует почти столько же
сколько и сама Ява. А именно напишем функцию sizeof() для объектов.
Желающие могут использовать свой любимый поисковик и
посмотреть(например по Java+sizeof), сколько и каких решений
предлагалось за последние годы, от использования Reflection до
вычисления размера занятой памяти и деления его на количество
объектов. Точного ответа при этом не давало, что интересно, ни одно.
Нам же достаточно просто прочитать поле class_struct.size
public static long sizeOf(Object object){
return unsafe.getAddress(unsafe.getAddress(ObjectToAddress(object)+4)+(vm1_5?12:56));
}
Функция возвращает результат в DWORD'ах, если нужен в байтах не
забудьте умножить на 4. С помощью sizeOf() можно теперь копировать
содержимое инстанций - так называемая "shallow copy". "Shallow" - так
как в случае внутренних переменных типа Object (и от него производных)
копируются естественно только указатели, а не объекты целиком.
public static void copyObjectShallow(Object objectSource, Object objectDest) {
unsafe.copyMemory(ObjectToAddress(objectSource),ObjectToAddress(objectDest),sizeOf(objectSource)*4);
}
copyObjectShallow бывает особенно полезна если иметь дело с объектами,
содержащими большое количество переменных примитивных типов. То есть
когда класс используется в основном для хранения данных, как структура
в С. Копировать переменные по одной (единственный штатный способ Явы)
- удовольствия мало.
Как известно, приведение типов в Яве осуществляется динамически, с
учетом иерархии классов. Бинарного каста (reinterpret_cast в терминах
С++) Ява к сожалению не поддерживает. Заполним этот пробел.
public static Object reinterpret_cast(Object o, Class cl){
unsafe.putAddress(ObjectToAddress(o)+4,unsafe.getAddress(ObjectToAddress(cl)+8));
return o;
}
reinterpret_cast возвращает указатель на объект о, приведенный к
заданному через параметер cl типу. Имеет ли такое преобразование
смысл, должен как всегда решать сам пользователь. Не следует только
забывать, что ошибка при подобных манипуляциях с памятью почти всегда
вызовет не безобидную Java Exception, а что-нибудь вроде Access
Violation в виртуальной машине. Классический случай применения
reinterpret_cast - когда один и тот же класс загружается два раза
двумя разными ClassLoader'ами.
Если собираетесь писать многопоточную программу, не забывайте о
синхронизации. Например имеет смысл добавить в определения приведенных
выше функций слово synchronized. Иначе может случиться так, что разные
потоки попытаются одновременно изменять структуры классов и радости
тогда будет много.
Исходный код класса UnsafeUtil вместе с примерами использования
отдельных его функций находится в приложении к статье.
5. Бесконечный final
Чтобы лучше оценить те практически неограниченные возможности, которые
открывает перед нами манипулирование структурами классов, разберем
пример посложнее.
Как известно, "final" в объявлении класса запрещает наследование от
него. Например классы типов, такие как String, Integer и т.д.
умышленно сделаны разработчиками языка конечными. Новички с завидным
постоянством спрашивают на форумах, можно ли написать собственный
класс строк, наследующий от String, и с таким же постоянством получают
ответ "нельзя". И тем не менее правильный ответ - можно.
Для простоты и наглядности возьмем функцию hashCode(). Как известно
хэш строк вычисляется по алгоритму
s[[ *31^(n-1)]] + s[[ *31^(n-2)]] + ... + s[n-1]
где n - длина строки и s[i] - i-й символ. Предположим теперь, что нас
не устраивает стандартный алгоритм и мы хотим вычислять сумму не с
начала строки, а с конца. Вот так:
s[n-1]*31^(n-1) + s[n-2]*31^(n-2) + ... + s[0]
То есть нужен класс, который наследует от String и имплементирует
новую hashCode(). И именно эти свойства имеет класс MagicString,
который можно найти в директории /string в исходниках к статье.
MagicStringWthStub реализует ту же самую идею, только чуть элегантнее,
например без использования Reflection. Недостатком в этом случае
является необходимость написания stub'а, что впрочем можно легко
автоматизировать. MagicString обходится без дополнительных классов.
Код самих классов я здесь приводить не хочу, чтобы не загромождать
статью. Основная идея состоит в том, что мы вносим String в список
суперклассов(смещения 24-52 + поле super_count) и подправляем поле
access_flags нужным образом (убираем final). Детали реализации можно
посмотреть в приложенных исходниках. Проверим теперь MagicString в
действии:
String s = (String)(Object)(new MagicStringWithStub("AB"));
System.out.println("Magic String hashcode: "+s.hashCode());
String s1 = new String("AB");
System.out.println("Standard String hashcode: "+s1.hashCode());
На выходе получим
Magic String hashcode: 2111
Standard String hashcode: 2081
Как видим, единственное небольшое неудобство состоит в том, что
кастить в String приходится через Object. Понятно почему:
компилятор-то о наших играх ничего не знает. В остальном мы полностью
достигли цели: получили String с нестандартным хэш-кодом.
В следующей статье (при условии, что у меня дойдут руки ее написать
:)) мы научимся создавать на Яве самомодифицирующийся код.
Благодарности
Quantum - за вдумчивое и терпеливое рецензирование черновых вариантов
статьи. Если я не последовал каким-либо его советам, то исключительно
по причине собственной лени.
Приложение
[[http://wasm.ru/pub/27/files/unsafe_java_1_code.zipunsafe_java_1_code.zip]] (7 KB) - Примеры к статье