funkcionálně.cz

Přední český blog o funkcionálním programování, kde se o funkcionálním programování nepíše
««« »»»

JVM a pohled objektům pod sukně

4. 6. 2013 (před 6 lety) — k47

V po­slední době jsem hodně psalpamětistruk­tuře ob­jektů na JVM. Abych sku­tečně ověřil, že to tak je, mám na výber ze dvou mož­ností: stu­do­vat zdro­jáky JVM nebo za­ho­dit bez­peč­nost a po­dí­vat se přímo na nahé bajty v paměti.

Já jsem dob­ro­druh a zvolil jsem va­ri­antu číslo dvě.

V JVM od Sunu/Oracle je k dis­po­zici třída sun.misc.Unsafe, jejíž jméno říká všechno po­třebné: ob­sa­huje ope­race, které nejsou bez­pečné, pro­tože při­stu­pují přímo k paměti, mohou číst, alo­ko­vat nebo uvol­ňo­vat li­bo­vol­nou paměť spra­vo­va­nou JVM a můžou skon­čit se­g­faul­tem.

No a právě Unsafe je ide­ální ná­stroj pro cestu do nitra ob­jektů. Pomocí metody getByte(object, offset) můžu číst syrové bajty ob­jektů včetně hla­vi­ček, které jsou před pro­gra­má­to­rem nor­málně skryté.

Tes­to­vací pro­gram je zá­le­ži­tost na pár řádků Scaly:

import sun.misc.Unsafe

val unsafe = {
  val f = classOf[Unsafe].getDeclaredField("theUnsafe")
  f.setAccessible(true)
  f.get().asInstanceOf[Unsafe]
}

val chars = "0123456789abcdef"
def byteToHexString(b: Byte) = ""+chars((b & 0xF0) >> 4)+chars(b & 0x0F)

// převede bajty na little endian
def reverseBytes(hexStr: String) = hexStr.grouped(2).toSeq.reverse.mkString

def intToHexString(int: Int) = {
  val id = Integer.toHexString(int)
  reverseBytes("0"*(8-id.length) + id)
}

// identity hash code
def idHashCode(x: AnyRef) = intToHexString(System.identityHashCode(x))

def objectBytes(x: AnyRef, bytes: Int) =
  0 until bytes map { i => byteToHexString(unsafe.getByte(x, i)) } mkString

Jako první si vy­tvo­řím třídu s ně­ko­lika atri­buty a po­dí­vám se jak je re­pre­zen­to­vaná v paměti. To stačí k tomu abych si ověřil pořadí atri­butů, pad­ding a jak jsou velké hla­vičky a co ob­sa­hují.

Všechny ukázky před­po­klá­dají Ope­n­JDK na 64 bi­to­vém pro­ce­soru s kom­pri­mo­va­nými re­fe­ren­cemi (OOP).

class X {
  val byte:  Byte     = 1
  val bool:  Boolean  = true
  val short: Short    = 2
  val char:  Char     = 3
  val int:   Int      = 4
  val long:  Long     = 5
  val ref:   Class[_] = this.getClass
}

val x = new X
idHashCode(x)
// b253ef01

objectBytes(x, 52) grouped 8 mkString "|"
// 01b253ef|01000000|800621e8|04000000|05000000|00000000|02000300|01010000|180921e8|00000000|05000000|00000000|9022f8e5
//   ^               ^        ^        ^                 ^   ^    ^ ^  ^   ^        ^        ^
//   hashCode        class    int      long              sh  ch   b b  XXX ref      XXXXXXXX next object

Z vý­stupu je krásně vidět ně­ko­lik věcí: iden­tity hash code začíná na 2. bajtu, po něm ná­sle­dují nějaké pří­znaky, zatím nulové, na 9. bajtu uka­za­tel na třídu ob­jektu a po dva­nác­ti­baj­tové hla­vičce ná­sle­dují sa­motné atri­buty ob­jektu.

Mnoho zdrojů na in­ter­netu udává, že hla­vička ob­jektu má 2 pro­ce­so­rová slova, tedy 8 nebo 16 bajtů. To platí na JVM bez kom­pri­mo­va­ných re­fe­rencí, ale když jsou COOP za­pnuté, hla­vička má 12 bajtů. To mi dlou­hou dobu ne­dá­valo smysl. Od­po­věď jsem našel až ve zdro­jo­vých kódech JVM. Hla­vička ob­jektu se skládá ze dvou částí: markOop (po­psáno zde) a classOop. markOop má délku jed­noho pro­ce­so­ro­vého slova a classOop má délku re­fe­rence. To do­hro­mady dává 8 bajtů pro 32 bit stroje, 12 bajtů pro 64 bit COOP stroje a 16 bajtů pro zbytek.

Na vy­psa­ných datech hla­vičky jsou ještě za­jí­mavé dvě věci: – Iden­tity hash code může být nulový dokud si ho ne­vy­žá­dám funkcí System.identityHashCode, teprve pak se do hla­vičky sku­tečně zapíše. JVM si tak nej­spíš šetří práci, pro­tože pro vět­šinu ob­jektů (nej­spíš) nebude nikdy po­třeba, pro­tože mají vlastní im­ple­men­taci ha­shCode. – Uka­za­tel na třídu ob­jektu v hla­vičce je jiný než když na stej­nou třídu od­ka­zuji ve vlast­ním atri­butu. Sho­dují se jenom po­slední dva bajty adresy. Všechny in­stance stejné třídy mají v hla­vičce stej­nou adresu, takže první dva bajty nejsou vy­čle­něny pro něco jiného, ale nějak se vzta­hují k adrese.

Na atri­bu­tech jsou patrná pra­vi­dla pro jejich řazení a pad­dingu:

  1. Každý objekt je za­rov­nán na ná­so­bek 8 bajtů.
  2. Atri­buty ob­jektů jsou řazeny podle ve­li­kosti: nejdřív long/double, pak int/float, char/shorts, byte/bo­o­lean a jako po­slední re­fe­rence na jiné ob­jekty. Atri­buty jsou vždy za­rov­nány na ná­so­bek vlastní ve­li­kosti.
  3. Atri­buty pa­t­řící různým třídám hi­e­rar­chie dě­dič­nosti se nikdy ne­mí­chají do­hro­mady. Atri­buty předka se v paměti na­chá­zejí před atri­buty po­tomků.
  4. První atri­but po­tomka musí být za­rov­nán na 4 bajty, takže za po­sled­ním atri­bu­tem předka může být až tří­baj­tová mezera.
  5. Pokud je první atri­but po­tomka long/double před kterým by za­rov­ná­ním vznikla čtyř­baj­tová mezera (předek není za­rov­nán na 8 bajtů, nebo ná­sle­dují po 12 baj­tové hla­vičce), long/double se může pře­su­nout tak, aby menší typy vy­pl­nily čtyř­baj­to­vou mezeru.

Dále jsem se po­dí­val na dě­dič­nost:

class Parent {
  val pi: Int  = 1
  val pb: Byte = 2
}
class Child extends Parent {
  val ci: Int  = 3
  val cl: Long = 4
}

val c = new Child
idHashCode(c) // 25e67300

objectBytes(c, 52) grouped 8 mkString "|"
// 0925e673|00000000|98d421e8|01000000|02000000|03000000|04000000|00000000|0d000000|00000000|7894c0e6|9da90200|d87f91fb
//   ^               ^        ^        ^ ^      ^        ^                 ^
//   hashCode        class    pi      pb XXXXXX ci       cl                next object

Zde je vidět, že se atri­buty předka a po­tomka ne­mí­chají a jsou za­rov­nány na 4 bajty.


Můžu si ověřit, jak jsou im­ple­men­to­vány něteré zá­kladní třídy jako třeba String.

val s = "0123456789"
idHashCode(s)              // 948e526c
intToHexString(s.hashCode) // 0546775e

objectBytes(s, 52) grouped 8 mkString "|"
// 01948e52|6c000000|b843a1e5|2026ffe7|00000000|0a000000|0546775e|00000000|01000000|00000000|1007a0e5|0a000000|30003100
//   ^               ^        ^        ^        ^        ^        ^        ^
//   hashCode        class    array    offset   length   hashCode XXXXXXXX next object

A pole:

val arr = Array[Long](1,2)
idHashCode(arr) // 631a605b

objectBytes(arr, 48) grouped 8 mkString "|"
// 09631a60|5b000000|5015a0e5|02000000|01000000|00000000|02000000|00000000|0d000000|00000000|e00225e8
//   ^               ^        ^        ^                 ^                 ^
//   hashCode        class    length   [1]               [2]               next object

Úplně na­ko­nec jsem se po­dí­val pod kapotu spe­ci­a­li­zo­va­ných třídy ze Scaly.

val s = (1, 2) // specializovaný tuple
s.getClass     // scala.Tuple2$mcII$sp
idHashCode(s)  // 69d5034e

objectBytes(s, 60) grouped 8 mkString "|"
// 0969d503|4e000000|c04828e6|00000000|00000000|01000000|02000000|00000000|0d000000|00000000|38f729e8|00000000|00000000|50cda5fb|0d000000
//   ^               ^        ^        ^        ^        ^        ^        ^
//   hashCode        class    _1       _2       _1 spec  _2 spec  XXXXXXXX next object

Jak je vidět, tak spe­ci­a­li­zo­vaná třída ob­sa­huje nejdřív atri­buty ge­ne­ric­kého předka za kte­rými ná­sle­dují jejich spe­ci­a­li­zo­vané pro­tějšky. Ge­ne­rické atri­buty _1 a _2 jsou null re­fe­rence a nejsou nikdy po­u­žity, spe­ci­a­li­zo­vané atri­buty _1 a _2 mají typ int a můžu přímo vidět jejich hod­noty.

@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články