Введение
Я помню, что это были солнечные выходные, солнце было красным, а цветы были разноцветными.В 1996 году ко мне пришел Puge WeChat и описал странную онлайн-проблему: онлайн-программа использовала динамическую память (HeapByteBuffer) NIO FileChannel. буфер, логика чтения и записи файлов можно сказать довольно простая, но по мониторингу установлено, что вздулась память вне кучи (DirectByteBuffer), в результате чего возникает исключение OutOfMemeory.
Этот онлайн-вопрос ведет к теме этой статьи, которая в основном включает в себя: анализ исходного кода FileChannel, мониторинг памяти вне кучи и восстановление памяти вне кучи.
Анализ проблем и анализ исходного кода
Ненормальное позиционирование журнала, найдено, что HeapByteBuffer действительно используется для чтения, но внешняя волна кучи свинца, а затем повернулся к источнику FileChannel, чтобы выяснить это.
FileChannel использует IOUtil для операций чтения и записи (в этой статье анализируется только логика чтения, логика кода записи и чтения непротиворечива, повторный анализ не выполняется)
//sun.nio.ch.IOUtil#read
static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if (var1.isReadOnly()) {
throw new IllegalArgumentException("Read-only buffer");
} else if (var1 instanceof DirectBuffer) {
return readIntoNativeBuffer(var0, var1, var2, var4);
} else {
ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());
int var7;
try {
int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
var5.flip();
if (var6 > 0) {
var1.put(var5);
}
var7 = var6;
} finally {
Util.offerFirstTemporaryDirectBuffer(var5);
}
return var7;
}
}
Можно обнаружить, что при использовании HeapByteBuffer он перейдет к следующей строке кода, которая кажется немного сомнительной:
Util.getTemporaryDirectBuffer(var1.remaining());
Эта утилита инкапсулирует некоторую низкоуровневую логику ввода-вывода.
package sun.nio.ch;
public class Util {
private static ThreadLocal<Util.BufferCache> bufferCache;
public static ByteBuffer getTemporaryDirectBuffer(int var0) {
if (isBufferTooLarge(var0)) {
return ByteBuffer.allocateDirect(var0);
} else {
// FOUCS ON THIS LINE
Util.BufferCache var1 = (Util.BufferCache)bufferCache.get();
ByteBuffer var2 = var1.get(var0);
if (var2 != null) {
return var2;
} else {
if (!var1.isEmpty()) {
var2 = var1.removeFirst();
free(var2);
}
return ByteBuffer.allocateDirect(var0);
}
}
}
}
Метод isBufferTooLarge будет определять, как выделить память вне кучи в соответствии с размером входящего буфера.Если он слишком велик, он будет напрямую выделять большой буфер; если он не слишком велик, он будет использовать переменную ThreadLocal bufferCache для cache и reuse (на самом деле это значение очень большое, почти никогда не уходит в ветку прямого выделения памяти вне кучи). Таким образом, кажется, что были найдены два удивительных вывода:
- Чтение и запись с использованием HeapByteBuffer будет проходить через DirectByteBuffer, поток записываемых данных на самом деле таков: HeapByteBuffer -> DirectByteBuffer -> PageCache -> Disk, а поток считываемых данных прямо противоположный.
- Использование считывателя HeapByteBuffer применит DirectByteBuffer с потоком привязки. Это означает, что чем больше потоков, тем больше временного DirectByteBuffer будет занимать больше места.
Увидев это, онлайн-проблема, по-видимому, имеет небольшое бровь: очень вероятно, что мульти-нить использует файлы записи HeapByteBuffer, и этот DirectbyteBuffer, который все назначен, вызвал переполнение памяти. Перед проверкой этого предположения, мы можем больше визуально следить за использованием депрессивной памяти, что может увеличить нашу веру.
Реализовать мониторинг памяти вне кучи
JDK предоставляет очень полезный инструмент мониторинга — Java VisualVM. Нам нужно только установить для него плагин, который может легко контролировать память вне кучи.
Войдите в исполняемый каталог локального JDK (в моей локальной области: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin), найдите команду jvisualvm, дважды щелкните, чтобы открыть визуальный интерфейс
Вы можете выбрать процесс Java для мониторинга в древовидном каталоге слева и информацию об измерениях для мониторинга справа.В дополнение к такой информации, как ЦП, поток, куча, класс и т. д., вы также можете установите подключаемые модули с помощью [Инструменты (T)] выше и добавьте MBeans, мониторинг буфера таких измерений, как пулы.
Плагин Buffer Pools может отслеживать память вне кучи (включая DirectByteBuffer и MappedByteBuffer), как показано на следующем рисунке:
Слева соответствует DirectbyteBuffer, а право соответствует maphaphyTebuffer.
Воспроизведите проблему
Чтобы воспроизвести онлайн-проблему, мы используем программу, которая постоянно запускает поток, чтобы использовать память в куче в качестве буфера для операций чтения файлов, и отслеживает использование памяти вне кучи процессом.
public class ReadByHeapByteBufferTest {
public static void main(String[] args) throws IOException, InterruptedException {
File data = new File("/tmp/data.txt");
FileChannel fileChannel = new RandomAccessFile(data, "rw").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(4 * 1024 * 1024);
for (int i = 0; i < 1000; i++) {
Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
try {
fileChannel.read(buffer);
buffer.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
После запуска в течение определенного периода времени мы наблюдаем использование памяти вне кучи.
Как показано слева на рисунке выше, объем памяти вне кучи действительно начал стремительно расти, что действительно соответствует нашим ожиданиям.Кэш вне кучи привязан к потокам.Когда потоков много, даже если используется только 4 М памяти в куче, это может привести к огромному ущербу. Произошло расширение памяти вне кучи и обрыв посередине. Предполагается, что выполнение потока или GC было завершено, что привело к освобождение памяти.
Зная это, я считаю, что вы можете уделять больше внимания использованию памяти в куче в будущем.Я резюмировал два момента:
- Для использования HeapByteBuffer также требуется копия DirectByteBuffer, чего можно избежать, напрямую мультиплексируя память вне кучи в сценариях, где требуется максимальная производительность.
- Используйте HeapByteBuffer для чтения и записи файлов в условиях многопоточности, обратите внимание
ThreadLocal<Util.BufferCache> bufferCacheВызвано проблемой расширения памяти вне кучи.
проблема в глубине
Вы когда-нибудь задумывались, почему JDK устроен именно так? Почему бы не использовать динамическую память напрямую для записи в PageCache, а затем удалить ее? Почему он должен проходить через копию DirectByteBuffer?
В связанных вопросах Zhihu, R YamatoЦзэн ЗетанДва студента ответили, и я согласен с этим объяснением:
Добавить Автора
Ссылка на сайт:Ууху. Call.com/question/57…
Источник: Чжиху
На самом деле это небольшая деталь реализации виртуальной машины HotSpot в OpenJDK.
GC в HotSpot VM перемещает объекты, кроме CMS, что называется «сжатием GC».
Если вы хотите передать ссылку на объект byte[] в Java в собственный код и позволить собственному коду напрямую обращаться к содержимому массива, вы должны убедиться, что объект byte[] не может быть перемещен, когда собственный код доступ к нему, то есть быть «закрепленным».
К сожалению, Hotspot VM решила не добиваться Object Pinning с уровня одного объекта.Если вы хотите PIN-код, вам нужно временно отключить GC - это равносильно приведению всей кучи Java к PIN-коду.
Так что место Oracle/Sun JDK/OpenJDK использует приложенный к изгибу. Предполагается, что содержимое BYTE[] позади HeapBytebuffer является накладными расходами времени, которые можно принять, и предполагается, что реальный ввод-вывод может быть очень медленной операцией.
Поэтому он сначала копирует содержимое byte[] позади HeapByteBuffer в родную память позади DirectByteBuffer.Это копирование будет включать вызов sun.misc.Unsafe.copyMemory(), за которым стоит реализация, аналогичная memcpy(). По сути, эта операция временно запрещает GC в течение всего процесса копирования.
Затем данные копируются в собственную память, и с ними легко обращаться, затем выполняется реальный ввод-вывод и передается адрес собственной памяти за DirectByteBuffer функции, которая выполняет реальный ввод-вывод. Нет необходимости обращаться к объектам Java для чтения и записи данных для ввода-вывода.
Подвести итог:
- Чтобы облегчить реализацию GC, собственная память, на которую указывает DirectByteBuffer, не управляется GC.
- HeapByteBuffer использует за собой массив байтов, а занимаемая им память не обязательно непрерывна, что не очень удобно для вызова методов JNI.
- Реализация массива может отличаться в разных JVM.
Освобождение памяти вне кучи
Продолжайте углубляться в следующую тему, которая также является вопросом, поднятым кем-то из моей группы обмена WeChat, как перезапустить DirectByteBuffer? Теперь, когда вы можете отслеживать память вне кучи, легко реализовать проверку восстановления памяти вне кучи.
CASE 1: Выделите 1G DirectByteBuffer, дождитесь ввода данных пользователем, скопируйте в null, затем заблокируйте и продолжайте наблюдать за изменениями в памяти вне кучи.
public class WriteByDirectByteBufferTest {
public static void main(String[] args) throws IOException, InterruptedException {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
System.in.read();
buffer = null;
new CountDownLatch(1).await();
}
}
Вывод: несмотря на то, что переменная имеет значение null, память продолжает занимать.
CASE 2: Выделите 1G DirectByteBuffer, дождитесь ввода данных пользователем, скопируйте в null, вручную запустите GC, а затем заблокируйте и продолжайте наблюдать за изменениями в памяти вне кучи.
public class WriteByDirectByteBufferTest {
public static void main(String[] args) throws IOException, InterruptedException {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
System.in.read();
buffer = null;
System.gc();
new CountDownLatch(1).await();
}
}
Вывод: GC вызовет восстановление свободной памяти за пределами кучи.
CASE 3: выделить 1G DirectByteBuffer, дождаться ввода пользователя, вручную освободить память вне кучи, а затем заблокировать и постоянно наблюдать за изменениями памяти вне кучи.
public class WriteByDirectByteBufferTest {
public static void main(String[] args) throws IOException, InterruptedException {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
System.in.read();
((DirectBuffer) buffer).cleaner().clean();
new CountDownLatch(1).await();
}
}
Вывод. Ручной сбор может немедленно освободить память вне кучи, не дожидаясь сборки мусора.
Для MappedByteBuffer, несколько загадочного класса, его механизм рециркуляции, вероятно, аналогичен механизму DirectByteBuffer, что отражено в Mapped справа.Мы не будем повторять тесты CASE1 и CASE2, а прямо дадим вывод, что при возникновении GC или операционная система активно очищает MappedByteBuffer, который будет переработан. Но не без тестирования проведем более интересное исследование MappedByteBuffer.
CASE 4: вручную перезапустить MappedByteBuffer.
public class MmapUtil {
public static void clean(MappedByteBuffer mappedByteBuffer) {
ByteBuffer buffer = mappedByteBuffer;
if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)
return;
invoke(invoke(viewed(buffer), "cleaner"), "clean");
}
private static Object invoke(final Object target, final String methodName, final Class<?>... args) {
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
try {
Method method = method(target, methodName, args);
method.setAccessible(true);
return method.invoke(target);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
});
}
private static Method method(Object target, String methodName, Class<?>[] args)
throws NoSuchMethodException {
try {
return target.getClass().getMethod(methodName, args);
} catch (NoSuchMethodException e) {
return target.getClass().getDeclaredMethod(methodName, args);
}
}
private static ByteBuffer viewed(ByteBuffer buffer) {
String methodName = "viewedBuffer";
Method[] methods = buffer.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("attachment")) {
methodName = "attachment";
break;
}
}
ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);
if (viewedBuffer == null)
return buffer;
else
return viewed(viewedBuffer);
}
}
Этот класс был описан в моем «Некоторых рекомендациях по файловому вводу-выводу», и здесь мы проверим, что он делает. Напишите тестовый класс:
public class WriteByMappedByteBufferTest {
public static void main(String[] args) throws IOException, InterruptedException {
File data = new File("/tmp/data.txt");
data.createNewFile();
FileChannel fileChannel = new RandomAccessFile(data, "rw").getChannel();
MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024L * 1024 * 1024);
System.in.read();
MmapUtil.clean(map);
new CountDownLatch(1).await();
}
}
Вывод: благодаря сложной операции отражения карта памяти Mmap была успешно восстановлена вручную.
CASE 5: проверить объем памяти Mmap.
public class WriteByMappedByteBufferTest {
public static void main(String[] args) throws IOException, InterruptedException {
File data = new File("/tmp/data.txt");
data.createNewFile();
FileChannel fileChannel = new RandomAccessFile(data, "rw").getChannel();
for (int i = 0; i < 1000; i++) {
fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024L * 1024 * 1024);
}
System.out.println("map finish");
new CountDownLatch(1).await();
}
}
Я попытался отобразить 1000G памяти, мой компьютер явно не имеет такой большой памяти 1000G, так как же обратная связь мониторинга?
Почти мгновенно консоль распечатывает лог завершения карты, а это также означает, что отображение 1000G памяти почти не занимает времени.Зачем проводить этот тест? Это делается для того, чтобы объяснить, что отображение памяти не равно использованию памяти.Многие статьи считают, что отображение памяти может значительно улучшить скорость чтения и записи файлов, и утверждают, что «запись MappedByteBuffer эквивалентна записи памяти», что на самом деле очень неправильное восприятие . Через панель управления вы можете просмотреть фактическую память, занятую процессом Java (pid 39040), которая составляет менее 100 МБ. (Сценарии и методы использования Mmap см. в моей предыдущей статье)
Вывод: После того, как MappedByteBuffer отобразит кусок содержимого файла, он не будет полностью загружен в память, а выполнит часть предварительного чтения (отражается в занятых 100M), MappedByteBuffer не панацея для чтения и записи файлов, он по-прежнему использует механизм асинхронной очистки PageCache.Размер общего отображения mmap можно отслеживать через Java VisualVM, но не фактический объем занимаемой памяти..
Суммировать
С помощью онлайн-вопроса в этой статье анализируется явление, при котором использование памяти в куче по-прежнему приводит к анализу памяти вне кучи и причина разработки JDK, а также используется инструмент Java VisualVM после установки. подключаемый модуль для мониторинга памяти вне кучи, а затем обсуждает, как правильно восстановить память вне кучи и исправить неправильное представление о MappedByteBuffer.
Добро пожаловать в мою общедоступную учетную запись WeChat: «Обмен технологиями Кирито». На любые вопросы по статье будут даны ответы, что позволит расширить обмен технологиями, связанными с Java.