Фактические накладные расходы памяти, подразумеваемые экземпляром объекта, зависят от некоторых внутренних деталей реализации JVM, и может быть трудно определить , поскольку оно может изменяться в течение всего времени жизни объекта (в сборщике мусора,объект может «перемещаться» между поколениями, которые используют различные структуры управления памятью).
Очень грубое приближение состоит в том, что каждый экземпляр любого объекта включает в себя два «слова» (два 32-разрядных значения на 32-разрядном компьютере).два 64-битных значения на 64-битных машинах);одно из слов более или менее является указателем на экземпляр Class
для этого объекта, другое содержит некоторое состояние объекта, например монитор для этого объекта (то, которое вы блокируете с помощью synchronized
).Тогда есть поля объекта.Для массива длина массива должна быть записана где-то в объекте, а также значения.
На этом этапе посмотрите на исходный код для классов Java (ищите файл с именем src.zip
в дистрибутиве JDK).В файле String.java
мы видим, что внутри экземпляра String
есть четыре поля: ссылка на массив значений char
и три int
(одно - индекс первого строкового символа вмассив, вторая - длина строки, а третья кэширует строковый хеш-код).Таким образом, для 32-разрядной машины можно оценить, что минимальное использование памяти для String
экземпляра n символов равно сумме:
- двух 32-разрядныхслова для
String
заголовка объекта экземпляра - четыре 32-разрядных слова для
String
поля экземпляра - три 32-разрядных слова для заголовка экземпляра массива и длины
- n 16-битные слова для самих символов (
char
- 16-битный)
Это только минимум, потому что экземпляр String
ссылается только на chunk внутреннего массива символов, поэтому размер памяти массива может быть больше.С другой стороны, массив символов может совместно использоваться несколькими экземплярами String
.Эта структура позволяет String.substring()
быть очень быстрым: новый экземпляр String
внутренне использует тот же массив, поэтому копирование данных не требуется;но это также означает, что если у вас есть большая строка, вы берете ее небольшую подстроку и сохраняете эту небольшую подстроку, вы на самом деле также сохраняете большой массив в ОЗУ (для экземпляра String
*)1043 *, вы можете сделать new String(str)
, чтобы получить новый экземпляр, который будет внутренне использовать вновь выделенный и урезанный экземпляр массива).С другой стороны, если у вас есть две строки, одна из которых является подстрокой другой, и вы храните обе строки в своем кэше, то вы платите только один раз за общий внутренний массив.
Следовательно, даже без учета всехСкрытые затраты, подразумеваемые GC, довольно трудно понять, что означает «размер памяти для строки»: если два экземпляра String
совместно используют один и тот же внутренний массив, как вы подсчитываете «размер» каждой строки?
Просмотр источника для HashMap
покажет вам, что есть внутренние экземпляры, которые также выделены;существует массив HashMap.Entry
экземпляров и один HashMap.Entry
экземпляров для каждого сохраненного значения.Размер массива динамически регулируется в зависимости от количества записей и настроенного коэффициента загрузки.
Поскольку учет объема памяти труден, совершенно другое решение состоит в том, чтобы позволить самому ГХ решать, когда старые записи кэша должны бытьудален.При этом внутренне используются «мягкие ссылки»: они являются своего рода указателями, которые GC может установить на null
, когда память становится тесной (разрыв ссылок может позволить GC освободить больше объектов).Это делает грубый кэш-память с поддержкой памяти, которая автоматически удаляется в зависимости от доступной памяти.Полезной библиотекой для этого является Google Guava и его MapMaker класс.