Sizeof за Java

26 декември 2003 г.

Въпрос: Има ли Java оператор като sizeof () в C?

A: A повърхностен отговор е, че Java не предвижда нещо подобно на C sizeof(). Нека обаче помислим защо програмист на Java понякога може да го пожелае.

Програмистът за променлив ток управлява повечето разпределения на паметта в структурата на данните и sizeof()е необходим за знанието на размерите на блоковете памет, които да се разпределят. Освен това разпределителите на памет C malloc()не правят почти нищо, що се отнася до инициализацията на обекти: програмистът трябва да зададе всички обектни полета, които са указатели, към следващи обекти. Но когато всичко е казано и кодирано, разпределението на паметта C / C ++ е доста ефективно.

За сравнение, разпределението и изграждането на обекти в Java са свързани (невъзможно е да се използва разпределен, но неинициализиран екземпляр на обект). Ако Java клас дефинира полета, които са препратки към други обекти, също е обичайно да се задават по време на изграждане. Следователно разпределянето на Java обект често разпределя множество взаимосвързани екземпляри на обект: графика на обект. Заедно с автоматичното събиране на боклука, това е твърде удобно и може да ви накара да се почувствате така, сякаш никога няма да се притеснявате за подробностите за разпределяне на паметта в Java.

Разбира се, това работи само за прости Java приложения. В сравнение с C / C ++, еквивалентните структури от данни на Java обикновено заемат повече физическа памет. При разработването на корпоративен софтуер приближаването до максимално наличната виртуална памет на днешните 32-битови JVM е често срещано ограничение за мащабируемост. По този начин, програмист на Java може да се възползва sizeof()или нещо подобно, за да следи дали неговите структури от данни стават твърде големи или съдържат тесни места в паметта. За щастие, отражението на Java ви позволява да напишете такъв инструмент доста лесно.

Преди да продължа, ще се откажа от някои чести, но неправилни отговори на въпроса на тази статия.

Заблуда: Sizeof () не е необходим, тъй като размерите на основните типове Java са фиксирани

Да, Java intе 32 бита във всички JVM и на всички платформи, но това е само изискване за спецификация на езика за възприеманата от програмиста ширина на този тип данни. По intсъщество такъв тип е абстрактен тип данни и може да бъде архивиран от, да речем, 64-битова дума от физическа памет на 64-битова машина. Същото важи и за непримитивните типове: спецификацията на езика Java не казва нищо за това как полетата на класа трябва да бъдат подравнени във физическата памет или че масив от булеви числа не може да бъде реализиран като компактен битвектор в JVM.

Заблуда: Можете да измерите размера на обекта, като го сериализирате в байтов поток и погледнете получената дължина на потока

Причината, поради която това не работи, е, че оформлението на сериализацията е само отдалечено отражение на истинското оформление в паметта. Един лесен начин да го видите е като разгледате как Strings сериализирате: в паметта всеки charе поне 2 байта, но в сериализирана форма Strings са кодирани UTF-8 и така всяко ASCII съдържание отнема наполовина по-малко място.

Друг работен подход

Може да си спомните „Java Tip 130: Знаете ли размера на данните си?“ който описва техника, базирана на създаване на голям брой идентични екземпляри от клас и внимателно измерване на резултантното увеличение на размера на използвания JVM купчина. Когато е приложимо, тази идея работи много добре и всъщност ще я използвам, за да стартирам алтернативния подход в тази статия.

Обърнете внимание, че Sizeofкласът на Java Tip 130 изисква спокоен JVM (така че активността на купчината се дължи само на разпределения на обекти и колекции боклук, поискани от измервателната нишка) и изисква голям брой идентични екземпляри на обекти. Това не работи, когато искате да оразмерите единичен голям обект (може би като част от изход за проследяване на грешки) и особено когато искате да проверите какво всъщност го е направило толкова голям.

Какъв е размерът на обекта?

Дискусията по-горе подчертава философски момент: като се има предвид, че обикновено се занимавате с обектни графики, каква е дефиницията на размера на обекта? Само размерът на екземпляра на обекта, който изследвате, или размерът на цялата графика с данни, вкоренени в екземпляра на обекта? Последното обикновено е по-важно на практика. Както ще видите, нещата не винаги са толкова ясни, но за начало можете да следвате този подход:

  • Екземпляр на обект може да бъде (приблизително) оразмерен чрез сумиране на всички негови нестатични полета с данни (включително полета, дефинирани в суперкласове)
  • За разлика от, да речем, C ++, методите на класа и тяхната виртуалност не оказват влияние върху размера на обекта
  • Суперинтерфейсите на класа не оказват влияние върху размера на обекта (вижте бележката в края на този списък)
  • Пълният размер на обекта може да се получи като затваряне върху цялата графика на обекта, вкоренена в началния обект
Забележка: Внедряването на всеки Java интерфейс просто маркира въпросния клас и не добавя никакви данни към дефиницията му. Всъщност JVM дори не потвърждава, че изпълнението на интерфейс осигурява всички методи, изисквани от интерфейса: това е строго отговорността на компилатора в настоящите спецификации.

За да стартирам процеса, за примитивни типове данни използвам физически размери, измерени от Sizeofкласа на Java Tip 130 . Както се оказва, за обичайните 32-битови JVM обикновеното java.lang.Objectзаема 8 байта, а основните типове данни обикновено са с най-малък физически размер, който може да отговори на езиковите изисквания (освен че booleanзаема цял байт):

// java.lang.Object размер на черупката в байтове: публичен статичен окончателен int OBJECT_SHELL_SIZE = 8; публичен статичен финал int OBJREF_SIZE = 4; публичен статичен финал int LONG_FIELD_SIZE = 8; публичен статичен финал int INT_FIELD_SIZE = 4; публичен статичен финал int SHORT_FIELD_SIZE = 2; публичен статичен финал int CHAR_FIELD_SIZE = 2; публичен статичен финал int BYTE_FIELD_SIZE = 1; публичен статичен финал int BOOLEAN_FIELD_SIZE = 1; публичен статичен финал int DOUBLE_FIELD_SIZE = 8; публичен статичен финал int FLOAT_FIELD_SIZE = 4;

(Важно е да се осъзнае, че тези константи не са кодирани завинаги и трябва да се измерват независимо за даден JVM.) Разбира се, наивното сумиране на размерите на обектните полета пренебрегва проблемите с подравняването на паметта в JVM. Изравняването на паметта има значение (както е показано, например, за примитивни типове масиви в Java Tip 130), но мисля, че е нерентабилно да се преследват такива подробности от ниско ниво. Тези детайли не само зависят от доставчика на JVM, но и не са под контрола на програмиста. Нашата цел е да получим добро предположение за размера на обекта и да се надяваме да разберем кога полето на класа може да е излишно; или когато полето трябва да бъде лено населено; или когато е необходима по-компактна вложена структура от данни и т.н. За абсолютна физическа точност винаги можете да се върнете към Sizeofкласа в Java Tip 130.

За да помогне на профила какво съставя екземпляр на обект, нашият инструмент не просто ще изчисли размера, но също така ще изгради полезна структура от данни като страничен продукт: графика, съставена от IObjectProfileNodes:

интерфейс IObjectProfileNode {Обект обект (); Име на низ (); int размер (); int refcount (); IObjectProfileNode родител (); IObjectProfileNode [] деца (); IObjectProfileNode черупка (); IObjectProfileNode [] път (); IObjectProfileNode корен (); int пътека (); булево пресичане (филтър INodeFilter, посетител INodeVisitor); Stump dump (); } // Край на интерфейса

IObjectProfileNodes са свързани помежду си по почти абсолютно същия начин като оригиналната графика на обекта, с IObjectProfileNode.object()връщане на реалния обект, който всеки възел представлява. IObjectProfileNode.size()връща общия размер (в байтове) на поддървото на обекта, вкоренено в екземпляра на обекта на този възел. Ако екземпляр на обект се свързва с други обекти чрез ненулеви полета на екземпляр или чрез препратки, съдържащи се в полетата на масива, тогава IObjectProfileNode.children()ще бъде съответстващ списък на дъщерни графични възли, сортирани в намаляващ ред. И обратно, за всеки възел, различен от началния, IObjectProfileNode.parent()връща своя родител. По IObjectProfileNodeтози начин цялата колекция от s нарязва и нарязва оригиналния обект и показва как съхранението на данни е разделено в него. Освен това имената на графичните възли се извличат от полетата на класа и изследват пътя на възела в графика (IObjectProfileNode.path()) ви позволява да проследявате връзките за собственост от оригиналния екземпляр на обект до всяка вътрешна част от данни.

Може да сте забелязали, докато четете предишния параграф, че идеята до момента все още има известна неяснота. Ако, докато обхождате графата на обекта, срещнете един и същ екземпляр на обект повече от веднъж (т.е. повече от едно поле някъде в графиката сочи към него), как да присвоите собствеността му (родителския указател)? Помислете за този кодов фрагмент:

 Обект obj = нов низ [] {нов низ ("JavaWorld"), нов низ ("JavaWorld")}; 

Each java.lang.String instance has an internal field of type char[] that is the actual string content. The way the String copy constructor works in Java 2 Platform, Standard Edition (J2SE) 1.4, both String instances inside the above array will share the same char[] array containing the {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} character sequence. Both strings own this array equally, so what should you do in cases like this?

If I always want to assign a single parent to a graph node, then this problem has no universally perfect answer. However, in practice, many such object instances could be traced back to a single "natural" parent. Such a natural sequence of links is usually shorter than the other, more circuitous routes. Think about data pointed to by instance fields as belonging more to that instance than to anything else. Think about entries in an array as belonging more to that array itself. Thus, if an internal object instance can be reached via several paths, we choose the shortest path. If we have several paths of equal lengths, well, we just pick the first discovered one. In the worst case, this is as good a generic strategy as any.

Мислейки за обхождане на графики и най-кратки пътища, би трябвало да звъни на този етап: търсенето с ширина първо е алгоритъм за обхождане на графика, който гарантира намирането на най-краткия път от началния възел до всеки друг достижим възел на графика.

След всички тези предварителни показания, ето учебната реализация на такава графика. (Някои подробности и спомагателни методи са пропуснати; вижте изтеглянето на тази статия за пълни подробности.):