Xpoint
   [напомнить пароль]

Пожалуйста, проголосуйте за скандальную ошибку Sun с мапированием

Метки: [без меток]
2007-02-11 12:09:30 [обр] Даниэль Алиевский(35/125)[досье]

Не знаю, как вам, уважаемые коллеги, а мне ошибка действительно кажется несколько скандальной. Хуже всего, что для моих сегодняшних разработок ошибка является весьма критичной. В сущности, я не могу корректно реализовать свой пакет, иначе как при помощи довольно некрасивых и недостаточно эффективных "заплат".

В сущности, речь идет о том, что мапированием файлов в Java пользоваться категорически нельзя.

По идее, FileChannel.map и MappedByteBuffer, реализованные еще в Java 1.4 - такой же штатный способ доступа к файлам, как и "обычный" RandomAccessFile. В некоторых случаях - жизненно необходимый для достижения нормальной производительности (когда требуется действительно произвольный и интенсивный доступ к файлу). И вот - применение FileChannel.map приводит к позорному вылетанию, причем на пользовательского потока, а всей JVM. Причем в столь тривиальной ситуации, что лично мне кажется - эта ошибка может сработать практически при любом способе применения мапирования, просто с разной вероятностью.

Подробно ошибка описана здесь:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6521677
Я завел сей баг главным образом ради подробного описания стабильно вылетающего тривиального теста, ибо в более сложных случаях ошибка была замечена аж в 2003 году:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4938372
Мой баг (6521677) объявлен дубликатом этого.

Если коротко, то проблему можно описать в нескольких строках кода:

... (f - переменная типа File, указывает на пока не существующий временный файл)
if (!f.exists())
    f.createNewFile();
RandomAccessFile raf = new RandomAccessFile(f, "rw");
raf.setLength((long)n * (long)size);
raf.close();

raf = new RandomAccessFile(f, "rw");
for (int i = 0; i < n; i++) {
    ByteBuffer bb = raf.getChannel().map(
        FileChannel.MapMode.READ_WRITE, (long)i * (long)size, size);
    for (int j = 0; j < bb.limit(); j++)
        ну, скажем, bb.put(j, (byte)i);
}
raf.close();

Т.е. просто последовательное мапирование блоков одного файла - и все! Причем размер блока size - степень двойки (скажем, 32768 или 2097152). Если запустить такой тест под Windows XP и последней JRE 1.6, то при достаточно большом числе блоков n и при нескольких повторениях указанного цикла поток сборки мусора породит исключение:

java.lang.Error: Cleaner terminated abnormally
     at sun.misc.Cleaner$1.run(Cleaner.java:130)
     at java.security.AccessController.doPrivileged(Native Method)
     at sun.misc.Cleaner.clean(Cleaner.java:127)
     at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:124)
Caused by: java.io.IOException: The process cannot access the file because another process has locked a portion of the file
     at sun.nio.ch.FileChannelImpl.unmap0(Native Method)
     at sun.nio.ch.FileChannelImpl.access$100(FileChannelImpl.java:32)
     at sun.nio.ch.FileChannelImpl$Unmapper.run(FileChannelImpl.java:680)
     at sun.misc.Cleaner.clean(Cleaner.java:125)
     ... 1 more

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

Вначале я подумал, что я просто "перенапрягаю" Windows очень большим количеством мапирований (число блоков n). Скажем, вряд ли безопасно мапировать десятки тысяч блоков, как и открывать десятки тысяч файлов. Но тестирование показало, что проблема явно не в этом. При size=32768 мне удавалось спровоцировать ошибку уже на 700-800 мапированных блоках. А при size=2097152 (2 MB) - в несколько видоизмененном тесте, где довольно часто вызывается System.gc() - проблема возникала даже при 10-20 одновременно смапированных блоках! (Правда, добиться этого непросто.)

Надо понимать, что Java работает с мапированием весьма нетривиально. Ведь в Java API в принципе отсутствует функция размапирования - этим должен заниматься сборщик мусора. Я специально "проследил" за финализацией объектов MappedByteBuffer с помощью очереди PhantomReference и даже попытался "принять меры", когда накапливается слишком много неразмапировнных блоков. А именно, приостановить работу в этой ситуации и начать выполнять цикл с System.gc(), пока все ненужные блоки не размапируются. Проблема в том, что ошибка иногда возникает, даже когда "слишком много" - это 10! А это значит, что проблема никак не в исчерпании ресурсов. Видимо, где-то в коде Sun сидит тяжелая ошибка, возможно, связанная с синхронизацией. Особенно подозрительно, что ошибка у меня ни разу не возникала, если файл нужного размера существовал до запуска теста: файл непременно должен быть только что создан. (Впрочем, для существующего файла возникали другие нелепые ошибки, например, невозможность вызвать System.runFinalization() или неожиданное Out of memory.)

Можно, конечно, понадеяться, что хотя бы простейшее мапирование (всего файла целиком) будет работать стабильно. Именно это советуют программисты Sun в том баге 4938372. Но, боюсь, этот совет - просто отписка. Ведь если JVM может упасть после нескольких десятков мапирований непересекающихся 2-мегабайтных кусков файла, то почему бы ей не упасть - пусть с меньшей вероятностью - после одного-единственного мапирования?

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

А мне, увы, как раз мапирование очень нужно. Я реализую абстракцию сверхбольшого массива произвольного доступа (с 64-битовой адресацией). Ни массивы Java, ни ByteBuffer в принципе (при текущем API) неспособны описать массив байтов, больший 2 GB. Даже в 64-разрядной Java. А сегодня большие изображения или трехмерные карты легко могут превысить этот предел. 8 GB RAM на компьютере и быстрый диск на 100-200 GB - это совсем недорого. Самое очевидное и эффективное решение (подходящее, к тому же, для 32-разрядной Java) - создать временный файл, изображающий "сверхбольшой массив", разбить на блоки (скажем, по 64 MB) и мапировать их в MappedByteBuffer по мере обращения к разным областям массива. Даже если доступ достаточно произвольный, это будет работать нормально, покуда размер файла не сильно превышает реальную RAM - ведь будет использоваться супер-интеллектуальный алгоритм подкачки страниц OS, имеющий в своем распоряжении всю оперативную память системы (а не только Java).

Конечно, покуда ошибка Sun не исправлена, это решение вряд ли удастся использовать. Буду писать собственный менеджер подкачек, который вместо мапирования будет просто читать и писать маленькие фрагменты файла. Совершенно очевидно, что по эффективности это будет чрезвычайно далеко от нормального решения с мапированием - по крайней мере в худших случаях. Но что делать.

А пока что большая просьба: пожалуйста, не пожалейте 10 минут, зарегистрируйтесь и проголосуйте за ошибку
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4938372
Если наберется несколько десятков голосов, ошибка может попасть в Top25 - глядишь, тогда Sun-овцы таки приложат усилия и исправят ошибку хотя бы к выпуску Java 1.7.

Аналог сего постинга опубликован на http://community.livejournal.com/ru_java/406335.html Из уважения к присутствующим, старался писать новыми словами, по возможности не копируя фрагменты текста :)

спустя 5 часов [обр] Pil(0/22)[досье]
Сделано.
спустя 17 часов [обр] Даниэль Алиевский(35/125)[досье]
Pil[досье] Спасибо. Надеюсь, будут еще голосования :)
спустя 6 минут [обр] GRAy(14/259)[досье]
Даниэль Алиевский[досье] Будут, будут ;)
спустя 2 дня [обр] Даниэль Алиевский(35/125)[досье]
Неужели это все? До попадания в top list всего десяток голосов осталось подать :) А лучше два десятка :)
спустя 18 минут [обр] GRAy(14/259)[досье]
На xpoint`е маловато java программеров ;) боюсь одним этим постингом и ЖЖ вы не поднимите достаточно народу. Попробуйте запостить ещё сюда.
спустя 23 часа [обр] Даниэль Алиевский(35/125)[досье]

> На xpoint`е маловато java программеров ;)
Чтобы проголосовать за ошибку, не надо быть java-программистом :) Я бы, например, прочитав подобный постинг, проголосовал бы за ошибку в PHP или Python (коих не знаю).

> Попробуйте запостить ещё сюда.
Спасибо, попробую.

спустя 2 часа [обр] Владимир Хоменко(2/67)[досье]
Проголосовал. А сколько нужно голосов для попадания в top list?
спустя 2 часа 3 минуты [обр] Даниэль Алиевский(35/125)[досье]

Спасибо. На данный момент top list кончается ошибкой с числом голосов 37. Так что, как бы, немного осталось. Но, понятно, чем больше, тем лучше. Хорошо бы хотя бы голосов 50 набрать.

С последней ошибкой из top-list они уже, кстати, 10 лет возятся. Interesting problem, понимаете ли :) (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4049083)

Вообще, мне это кажется каким-то странным. Неужели исправить 25 "популярных" ошибок - это настолько сложно, что требуются десятилетия?

спустя 9 дней [обр] Даниэль Алиевский(35/125)[досье]

Всем спасибо за голосование, ошибка в топ-листе. Но, гм... У меня тут на очереди еще 3 ошибки. Скоро отправлю в Sun. Наверно, просить голосовать за все - это уже перебор :)

Кстати, work-around для данной ошибки - вовремя вызывать метод force() у MappedByteBuffer. Перед "забыванием" ссылки. Правда, сие, увы, негативно сказывается на скорости - ведь это означает принудительную запись данных на диск.

спустя 23 дня [обр] Даниэль Алиевский(35/125)[досье]

Спасибо всем за голосование. Похоже, возымело эффект: утверждают, что исправлено в Java 1.7 b10. Ждем, когда выложат эту версию...

Тему можно закрывать.

спустя 24 дня [обр] Даниэль Алиевский(35/125)[досье]
И правда исправили. Правда, в результате исправления система начала жестоко умирать в свопинге при попытке последовательно, без затей смапировать несколько гигабайт, о чем мы сейчас ведем с сановцами... опс, с уважаемыми сотрудниками компании Sun интенсивную переписку. Но по крайней мере эту проблему можно успешно обойти, приложив некоторый объем усилий - в отличие от бывшей ошибки 4938372. Если ситуация изменится или появится публичное обсуждение, напишу.
Powered by POEM™ Engine Copyright © 2002-2005