Статьи

Печально известное sun.misc.Unsafe объяснил

Крупнейшим конкурентом виртуальной машины Java может быть CLR от Microsoft, на котором размещены такие языки, как C #. CLR позволяет писать небезопасный код в качестве шлюза для низкоуровневого программирования, чего трудно достичь в JVM. Если вам нужны такие расширенные функциональные возможности в Java, вы можете быть вынуждены использовать JNI, который требует от вас знания некоторого C и быстро приведет к коду, тесно связанному с конкретной платформой. Однако с sun.misc.Unsafe существует еще одна альтернатива низкоуровневому программированию на платформе Java с использованием Java API, хотя эта альтернатива не рекомендуется . Тем не менее, некоторые приложения полагаются на sun.misc.Unsafe , например, objenesis и вместе с тем все библиотеки, основанные на последнем, такие как, например, kryo, который снова используется, например, в Twitter-шторме . Поэтому пришло время взглянуть, тем более что функциональность sun.misc.Unsafe считается частью открытого API Java в Java 9 .

Получение экземпляра sun.misc.Unsafe

Класс sun.misc.Unsafe предназначен для использования только основными классами Java, поэтому его авторы сделали свой единственный конструктор частным и добавили только одинаково частный экземпляр-одиночка. Публичный получатель для этих экземпляров выполняет проверку безопасности, чтобы избежать его публичного использования:

1
2
3
4
5
6
public static Unsafe getUnsafe() {
  Class cc = sun.reflect.Reflection.getCallerClass(2);
  if (cc.getClassLoader() != null)
    throw new SecurityException("Unsafe");
  return theUnsafe;
}

Этот метод сначала ищет вызывающий Class из стека методов текущего потока. Этот поиск реализован другим внутренним классом с именем sun.reflection.Reflection который в основном просматривает заданное количество кадров стека вызовов, а затем возвращает определяющий класс этого метода. Однако эта проверка безопасности может измениться в будущей версии . При просмотре стека первым найденным классом (индекс 0 ), очевидно, будет сам класс Reflection , а вторым (индекс 1 ) будет класс Unsafe , так что в индексе 2 будет храниться класс приложения, который вызывал Unsafe#getUnsafe()

Этот ClassLoader класс затем проверяется на предмет его ClassLoader где null ссылка используется для представления загрузчика класса начальной загрузки на виртуальной машине HotSpot. (Это описано в Class#getClassLoader() где говорится, что « некоторые реализации могут использовать null для представления загрузчика класса начальной загрузки ».) Поскольку никакой неосновной Java-класс обычно никогда не загружается с этим загрузчиком классов, вы никогда не будете возможность вызывать этот метод напрямую, но в качестве ответа получает выброшенное исключение SecurityException (Технически вы можете заставить ВМ загружать ваши классы приложений, используя загрузчик классов начальной загрузки, добавив его в –Xbootclasspath , но это потребует некоторой настройки вне кода вашего приложения, чего вы можете избежать.) Таким образом, следующий тест удастся:

1
2
3
4
@Test(expected = SecurityException.class)
public void testSingletonGetter() throws Exception {
  Unsafe.getUnsafe();
}

Тем не менее, проверка безопасности плохо спроектирована и должна рассматриваться как предупреждение против одноэлементного анти-паттерна . До тех пор, пока использование отражения не запрещено (что сложно, так как оно широко используется во многих средах), вы всегда можете получить экземпляр, осмотрев приватных членов класса. Из исходного кода класса Unsafe вы можете узнать, что экземпляр singleton хранится в закрытом статическом поле, называемом theUnsafe . Это как минимум верно для виртуальной машины HotSpot. К сожалению для нас, другие реализации виртуальных машин иногда используют другие имена для этого поля. Класс Unsafe Android, например, хранит свой экземпляр-одиночка в поле с именем THE_ONE . Это затрудняет предоставление «совместимого» способа получения экземпляра. Однако, поскольку мы уже покинули сохраняемую область совместимости, используя класс Unsafe , нам не следует беспокоиться об этом больше, чем об этом. Чтобы получить экземпляр singleton, вы просто читаете значение поля singleton:

1
2
3
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

Кроме того, вы можете вызвать частного инструктора. Я лично предпочитаю этот способ, так как он работает, например, с Android, а извлечение поля не:

1
2
3
Constructor<Unsafe> unsafeConstructor = Unsafe.class.getDeclaredConstructor();
unsafeConstructor.setAccessible(true);
Unsafe unsafe = unsafeConstructor.newInstance();

Цена, которую вы платите за это незначительное преимущество совместимости, — это минимальный объем кучи. Однако проверки безопасности, выполняемые при использовании отражения в полях или конструкторах, аналогичны.

Создать экземпляр класса без вызова конструктора

В первый раз я использовал класс Unsafe для создания экземпляра класса без вызова какого-либо из конструкторов класса. Мне нужно было проксировать весь класс, у которого был только довольно шумный конструктор, но я только хотел делегировать все вызовы методов реальному экземпляру, который, однако, я не знал во время создания. Создать подкласс было легко, и если бы класс был представлен интерфейсом, создание прокси было бы простой задачей. Однако с дорогим конструктором я застрял. Используя класс Unsafe, я смог обойти его. Рассмотрим класс с искусственно дорогим конструктором:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class ClassWithExpensiveConstructor {
 
  private final int value;
 
  private ClassWithExpensiveConstructor() {
    value = doExpensiveLookup();
  }
 
  private int doExpensiveLookup() {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return 1;
  }
 
  public int getValue() {
    return value;
  }
}

Используя Unsafe , мы можем создать экземпляр ClassWithExpensiveConstructor (или любого из его подклассов), не ClassWithExpensiveConstructor вышеуказанный конструктор, просто выделив экземпляр непосредственно в куче:

1
2
3
4
5
6
@Test
public void testObjectCreation() throws Exception {
  ClassWithExpensiveConstructor instance = (ClassWithExpensiveConstructor)
  unsafe.allocateInstance(ClassWithExpensiveConstructor.class);
  assertEquals(0, instance.getValue());
}

Обратите внимание, что поле final осталось неинициализированным конструктором, но для него установлено значение по умолчанию для его типа . Кроме этого, построенный экземпляр ведет себя как обычный объект Java. Это будет, например, мусор, когда он станет недоступным.

Среда выполнения Java сама создает объекты без вызова конструктора, например, при создании объектов для десериализации. Следовательно, ReflectionFactory предлагает еще больший доступ к созданию отдельных объектов:

1
2
3
4
5
6
7
8
@Test
public void testReflectionFactory() throws Exception {
  @SuppressWarnings("unchecked")
  Constructor<ClassWithExpensiveConstructor> silentConstructor = ReflectionFactory.getReflectionFactory()
      .newConstructorForSerialization(ClassWithExpensiveConstructor.class, Object.class.getConstructor());
  silentConstructor.setAccessible(true);
  assertEquals(10, silentConstructor.newInstance().getValue());
}

Обратите внимание, что для класса ReflectionFactory требуется только RuntimePermission именем RuntimePermission для получения его экземпляра- RuntimePermission и поэтому здесь не требуется никакого отражения. Полученный экземпляр ReflectionFactory позволяет вам определить любой конструктор, который станет конструктором для данного типа. В приведенном выше примере я использовал конструктор по умолчанию java.lang.Object для этой цели. Однако вы можете использовать любой конструктор:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class OtherClass {
 
  private final int value;
  private final int unknownValue;
 
  private OtherClass() {
    System.out.println("test");
    this.value = 10;
    this.unknownValue = 20;
  }
}
 
@Test
public void testStrangeReflectionFactory() throws Exception {
  @SuppressWarnings("unchecked")
  Constructor<ClassWithExpensiveConstructor> silentConstructor = ReflectionFactory.getReflectionFactory()
      .newConstructorForSerialization(ClassWithExpensiveConstructor.class,
            OtherClass.class.getDeclaredConstructor());
  silentConstructor.setAccessible(true);
  ClassWithExpensiveConstructor instance = silentConstructor.newInstance();
  assertEquals(10, instance.getValue());
  assertEquals(ClassWithExpensiveConstructor.class, instance.getClass());
  assertEquals(Object.class, instance.getClass().getSuperclass());
}

Обратите внимание, что value было установлено в этом конструкторе, хотя был вызван конструктор совершенно другого класса. Однако несуществующие поля в целевом классе игнорируются, что также очевидно из приведенного выше примера. Обратите внимание, что OtherClass не становится частью иерархии типов OtherClass экземпляров, конструктор OtherClass просто заимствуется для «сериализованного» типа.

В этой записи блога не упоминаются другие методы, такие как Unsafe#defineClass , Unsafe#defineAnonymousClass или Unsafe#ensureClassInitialized . Подобная функциональность, однако, также определена в общедоступном API ClassLoader .

Собственное распределение памяти

Вы когда-нибудь хотели выделить массив в Java, который должен был бы иметь больше, чем записи Integer.MAX_VALUE ? Вероятно, не потому, что это не обычная задача, но если вам когда-то понадобится эта функциональность, это возможно. Вы можете создать такой массив, выделив собственную память . Собственное распределение памяти используется, например, прямыми байтовыми буферами , которые предлагаются в пакетах Java NIO . Помимо памяти кучи, встроенная память не является частью области кучи и может использоваться не исключительно, например, для связи с другими процессами. В результате пространство кучи Java конкурирует с собственным пространством: чем больше памяти вы назначаете JVM, тем меньше остается собственной памяти.

Давайте рассмотрим пример использования собственной (вне кучи) памяти в Java при создании упомянутого массива увеличенного размера:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DirectIntArray {
 
  private final static long INT_SIZE_IN_BYTES = 4;
 
  private final long startIndex;
 
  public DirectIntArray(long size) {
    startIndex = unsafe.allocateMemory(size * INT_SIZE_IN_BYTES);
    unsafe.setMemory(startIndex, size * INT_SIZE_IN_BYTES, (byte) 0);
    }
  }
 
  public void setValue(long index, int value) {
    unsafe.putInt(index(index), value);
  }
 
  public int getValue(long index) {
    return unsafe.getInt(index(index));
  }
 
  private long index(long offset) {
    return startIndex + offset * INT_SIZE_IN_BYTES;
  }
 
  public void destroy() {
    unsafe.freeMemory(startIndex);
  }
}
 
@Test
public void testDirectIntArray() throws Exception {
  long maximum = Integer.MAX_VALUE + 1L;
  DirectIntArray directIntArray = new DirectIntArray(maximum);
  directIntArray.setValue(0L, 10);
  directIntArray.setValue(maximum, 20);
  assertEquals(10, directIntArray.getValue(0L));
  assertEquals(20, directIntArray.getValue(maximum));
  directIntArray.destroy();
}

Сначала убедитесь, что на вашем компьютере достаточно памяти для запуска этого примера! Вам нужно как минимум (2147483647 + 1) * 4 byte = 8192 MB встроенной памяти для запуска кода. Если вы работали с другими языками программирования, такими как, например, C, прямое распределение памяти — это то, что вы делаете каждый день. Unsafe#allocateMemory(long) , виртуальная машина выделяет запрошенный объем встроенной памяти для вас. После этого вы будете обязаны правильно обращаться с этой памятью.

Объем памяти, необходимый для хранения определенного значения, зависит от размера типа. В приведенном выше примере я использовал тип int который представляет 32-разрядное целое число. Следовательно, одно значение типа int занимает 4 байта. Для примитивных типов размер хорошо документирован . Однако более сложно вычислить размер типов объектов, поскольку они зависят от числа нестатических полей, которые объявлены где-либо в иерархии типов. Наиболее каноническим способом вычисления размера объекта является использование класса Instrumented из API присоединения Java, который предлагает специальный метод для этой цели, называемый getObjectSize . Однако в конце этого раздела я оценю другой (хакерский) способ работы с объектами.

Имейте в виду, что непосредственно выделенная память всегда является встроенной памятью и поэтому не является сборщиком мусора. Поэтому вам необходимо явно освободить память, как показано в примере выше, путем вызова Unsafe#freeMemory(long) . В противном случае вы зарезервировали некоторую память, которую никогда не сможете использовать для чего-то другого, пока экземпляр JVM работает, что является утечкой памяти и распространенной проблемой в языках без сбора мусора. Кроме того, вы также можете напрямую перераспределить память по определенному адресу, вызвав Unsafe#reallocateMemory(long, long) где второй аргумент описывает новое количество байтов, которое должно быть зарезервировано JVM по данному адресу.

Также обратите внимание, что непосредственно выделенная память не инициализируется с определенным значением. Как правило, вы обнаружите мусор из-за старого использования этой области памяти, так что вам придется явно инициализировать выделенную память, если вам требуется значение по умолчанию. Это то, что обычно делается для вас, когда вы позволяете среде выполнения Java выделять память для вас. В приведенном выше примере вся область заменяется нулями с помощью метода Unsafe#setMemory .

При использовании памяти, выделенной напрямую, JVM также не будет выполнять проверку диапазона. Поэтому возможно испортить вашу память, как показано в следующем примере:

01
02
03
04
05
06
07
08
09
10
@Test
public void testMallaciousAllocation() throws Exception {
  long address = unsafe.allocateMemory(2L * 4);
  unsafe.setMemory(address, 8L, (byte) 0);
  assertEquals(0, unsafe.getInt(address));
  assertEquals(0, unsafe.getInt(address + 4));
  unsafe.putInt(address + 1, 0xffffffff);
  assertEquals(0xffffff00, unsafe.getInt(address));
  assertEquals(0x000000ff, unsafe.getInt(address + 4));
}

Обратите внимание, что мы записали значение в пространство, каждое из которых было частично зарезервировано для первого и второго числа. Эта картина может прояснить ситуацию. Имейте в виду, что значения в памяти запускаются «справа налево» (но это может зависеть от машины).

без названия

Первая строка показывает начальное состояние после записи нулей во всю выделенную область собственной памяти. Затем мы переопределяем 4 байта со смещением одного байта, используя 32. Последняя строка показывает результат после этой операции записи.

Наконец, мы хотим записать весь объект в собственную память. Как упоминалось выше, это сложная задача, так как сначала нам нужно вычислить размер объекта, чтобы узнать размер, который нам нужно зарезервировать. Класс Unsafe, однако, не предлагает такой функциональности. По крайней мере, не напрямую, так как мы можем, по крайней мере, использовать класс Unsafe, чтобы найти смещение поля экземпляра, которое используется JVM, когда он сам размещает объекты в куче. Это позволяет нам найти приблизительный размер объекта:

01
02
03
04
05
06
07
08
09
10
11
public long sizeOf(Class<?> clazz)
  long maximumOffset = 0;
  do {
    for (Field f : clazz.getDeclaredFields()) {
      if (!Modifier.isStatic(f.getModifiers())) {
        maximumOffset = Math.max(maximumOffset, unsafe.objectFieldOffset(f));
      }
    }
  } while ((clazz = clazz.getSuperclass()) != null);
  return maximumOffset + 8;
}

На первый взгляд это может показаться загадочным, но за этим кодом нет большого секрета. Мы просто перебираем все нестатические поля, которые объявлены в самом классе или в любом из его суперклассов. Нам не нужно беспокоиться об интерфейсах, поскольку они не могут определять поля и поэтому никогда не изменят структуру памяти объекта. Любое из этих полей имеет смещение, которое представляет первый байт, который занят значением этого поля, когда JVM хранит экземпляр этого типа в памяти, относительно первого байта, который используется для этого объекта. Нам просто нужно найти максимальное смещение, чтобы найти пространство, необходимое для всех полей, кроме последнего. Поскольку поле никогда не будет занимать более 64 бит (8 байт) для long или double значения или для ссылки на объект при запуске на 64-битной машине, мы по крайней мере нашли верхнюю границу для пространства, которое используется для хранения объект. Поэтому мы просто добавляем эти 8 байтов к максимальному индексу, и мы не столкнемся с опасностью зарезервировать мало места. Эта идея, конечно, тратит немного байта, и для производственного кода следует использовать лучший алгоритм.

В этом контексте лучше всего рассматривать определение класса как форму гетерогенного массива. Обратите внимание, что минимальное смещение поля не 0 а положительное значение. Первые несколько байтов содержат метаинформацию. На рисунке ниже представлен этот принцип для примера объекта с int и long полем, в котором оба поля имеют смещение. Обратите внимание, что мы обычно не записываем метаинформацию при записи копии объекта в собственную память, поэтому мы можем еще больше сократить количество используемых собственных заметок. Также обратите внимание, что эта схема памяти может сильно зависеть от реализации виртуальной машины Java.

object_layout

С этой чрезмерно осторожной оценкой мы можем теперь реализовать некоторые методы-заглушки для записи мелких копий объектов непосредственно в собственную память. Обратите внимание, что собственная память на самом деле не знает концепцию объекта. В основном мы просто устанавливаем заданное количество байтов для значений, которые отражают текущие значения объекта. Пока мы помним расположение памяти для этого типа, эти байты содержат, однако, достаточно информации для восстановления этого объекта.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void place(Object o, long address) throws Exception {
  Class clazz = o.getClass();
  do {
    for (Field f : clazz.getDeclaredFields()) {
      if (!Modifier.isStatic(f.getModifiers())) {
        long offset = unsafe.objectFieldOffset(f);
        if (f.getType() == long.class) {
          unsafe.putLong(address + offset, unsafe.getLong(o, offset));
        } else if (f.getType() == int.class) {
          unsafe.putInt(address + offset, unsafe.getInt(o, offset));
        } else {
          throw new UnsupportedOperationException();
        }
      }
    }
  } while ((clazz = clazz.getSuperclass()) != null);
}
 
public Object read(Class clazz, long address) throws Exception {
  Object instance = unsafe.allocateInstance(clazz);
  do {
    for (Field f : clazz.getDeclaredFields()) {
      if (!Modifier.isStatic(f.getModifiers())) {
        long offset = unsafe.objectFieldOffset(f);
        if (f.getType() == long.class) {
          unsafe.putLong(instance, offset, unsafe.getLong(address + offset));
        } else if (f.getType() == int.class) {
          unsafe.putLong(instance, offset, unsafe.getInt(address + offset));
        } else {
          throw new UnsupportedOperationException();
        }
      }
    }
  } while ((clazz = clazz.getSuperclass()) != null);
  return instance;
}
 
@Test
public void testObjectAllocation() throws Exception {
  long containerSize = sizeOf(Container.class);
  long address = unsafe.allocateMemory(containerSize);
  Container c1 = new Container(10, 1000L);
  Container c2 = new Container(5, -10L);
  place(c1, address);
  place(c2, address + containerSize);
  Container newC1 = (Container) read(Container.class, address);
  Container newC2 = (Container) read(Container.class, address + containerSize);
  assertEquals(c1, newC1);
  assertEquals(c2, newC2);
}

Обратите внимание, что эти методы-заглушки для записи и чтения объектов в собственной памяти поддерживают только значения int и long . Конечно, Unsafe поддерживает все примитивные значения и может даже записывать значения, не затрагивая локальные потоки кэшей, используя изменчивые формы методов. Заглушки использовались только для краткости примеров. Имейте в виду, что эти «экземпляры» никогда не будут собирать мусор, поскольку их память была выделена напрямую. (Но, возможно, это именно то, что вам нужно.) Кроме того, будьте осторожны при предварительном расчете размера, поскольку компоновка памяти объекта может зависеть от виртуальной машины, а также изменять, если ваш 64-разрядный компьютер выполняет код по сравнению с 32-разрядным компьютером. Смещения могут даже измениться между перезапусками JVM.

Для чтения и записи примитивов или ссылок на объекты Unsafe предоставляет следующие методы, зависящие от типа:

  • getXXX(Object target, long offset) : будет считывать значение типа XXX из адреса цели с указанным смещением.
  • putXXX(Object target, long offset, XXX value) : поместит значение по адресу цели в указанное смещение.
  • getXXXVolatile(Object target, long offset) : будет читать значение типа XXX из адреса цели с указанным смещением и не попадать ни в какие локальные кэши потоков.
  • putXXXVolatile(Object target, long offset, XXX value) : поместит значение по адресу цели в указанное смещение и не затронет локальные кэши потоков.
  • putOrderedXXX(Object target, long offset, XXX value) : поместит значение по адресу цели в указанном offet и может не попасть во все локальные кэши потоков.
  • putXXX(long address, XXX value) : поместит указанное значение типа XXX непосредственно по указанному адресу.
  • getXXX(long address) : будет считывать значение типа XXX с указанного адреса.
  • compareAndSwapXXX(Object target, long offset, long expectedValue, long value) : будет атомарно считывать значение типа XXX из адреса цели с указанным смещением и устанавливать данное значение, если текущее значение с этим смещением равно ожидаемому значению.

Помните, что вы копируете ссылки при записи или чтении копий объектов в собственной памяти, используя семейство getObject(Object, long) . Поэтому вы применяете только мелкие копии экземпляров, когда применяете вышеуказанный метод. Однако вы всегда можете рекурсивно читать размеры и смещения объектов и создавать глубокие копии. Однако обратите внимание на циклические ссылки на объекты, которые могут вызвать бесконечные циклы при небрежном применении этого принципа.

Здесь не упоминаются существующие утилиты в классе Unsafe, которые позволяют манипулировать значениями статического поля, такими как staticFieldOffset и для обработки типов массивов. Наконец, оба метода с именем Unsafe#copyMemory позволяют Unsafe#copyMemory прямую копию памяти, либо относительно определенного смещения объекта, либо по абсолютному адресу, как показано в следующем примере:

1
2
3
4
5
6
7
8
@Test
public void testCopy() throws Exception {
  long address = unsafe.allocateMemory(4L);
  unsafe.putInt(address, 100);
  long otherAddress = unsafe.allocateMemory(4L);
  unsafe.copyMemory(address, otherAddress, 4L);
  assertEquals(100, unsafe.getInt(otherAddress));
}

Бросать проверенные исключения без объявления

В Unsafe есть и другие интересные методы. Вы когда-нибудь хотели выбросить определенное исключение для обработки на более низком уровне, но вы высокоуровневый тип интерфейса не объявили это проверенное исключение? Unsafe#throwException позволяет сделать это:

1
2
3
4
5
6
7
8
@Test(expected = Exception.class)
public void testThrowChecked() throws Exception {
  throwChecked();
}
 
public void throwChecked() {
  unsafe.throwException(new Exception());
}

Родной параллелизм

Методы park и unpark позволяют приостановить поток на некоторое время и возобновить его:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testPark() throws Exception {
  final boolean[] run = new boolean[1];
  Thread thread = new Thread() {
    @Override
    public void run() {
      unsafe.park(true, 100000L);
      run[0] = true;
    }
  };
  thread.start();
  unsafe.unpark(thread);
  thread.join(100L);
  assertTrue(run[0]);
}

Кроме того, мониторы могут быть получены напрямую с помощью Unsafe с использованием monitorEnter(Object) , monitorExit(Object) и tryMonitorEnter(Object) .

Ссылка: печально известный sun.misc.Unsafe объяснил наш партнер JCG Рафаэль Винтерхальтер в блоге My daily Java .