JVM: Epizoda V – Paměť vrací úder

Minule jsem psal, jak se podívat objektům pod sukně a přímo přečíst bajty v jejich hlavičkách. Už to samo o sobě jde daleko za hranice virtuálního stroje a narušuje jeho paměťovou bezpečnost. Takže komu se dělalo minule mdlo, by vůbec neměl číst tento článek, protože jdu ještě o kus dál a pokouším se zjistit, co by se dalo dělat, kdybych do hlaviček objektů mohl zapisovat.

Nejde o nic komplikovaného, třída sun.misc.Unsafe má kromě metod get{Byte, Int, Long, …} i sadu metod put{Byte, Int, Long, …}, které mohou zapsat na libovolné místo paměti (a když ta paměť nepatří JVM, skončí segfaultem).

Napadly mě čtyři případy, kdy by se tento hrozný hack dal využít. Ale neříkám, že by se někdy měl použít, protože je závislý na konkrétním JVM a konkrétní architektuře a už z principu jde o nebezpečný, nepřenositelný a nepředvídatelný kód, který může způsobit bizarní nevystopovatel­né chyby.

Kdyby se někdo ptal, zapírejte, popřete, že jste cokoli z následujících řádků kdy četli nebo že vás to vůbec napadlo. Nesmíte dát najevo, že jste se nechali omámit zakázaným ovocem a přešli na temnou stranu programování.

1) Změna třídy objektu

Pomocí unsafe můžu celkem jednoduše změnit třídu objektu. Stačí vědět, že OpenJDK uchovává pointer na třídu těsně za sekcí markOop (viz. minule), na 32bit platformách je na offsetu 4, na 64 bit platformách na offsetu 8. Odkaz na třídu je uložen nějak zvláštně a proto nemůžu použít libovolnou referenci, ale musím ji zkopírovat z jiné instance.

import sun.misc.Unsafe

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

class A {
  val l: Long = 0x0000000100000002L
}
class B {
  val a: Int = 0
  val b: Int = 0
  def aa = a
  def bb = b
}

val a = new A
val b = new B

// změna třídy proběhne zde
unsafe.putInt(a, 8, unsafe.getInt(b, 8))

// bez problémů přetypuji na novou třídu
val a_b = a.asInstanceOf[B]
a_b.getClass // B
// přidané metody fungují bez problémů
a_b.aa // 0 - padding
a_b.bb // 2 - první 4 bajty longu

2) Zkracování polí

Délka pole je uložena hned po ukazateli na třídu objektu, tedy na dobře známém a stabilním místě (které se ale pochopitelně liší podle architektury). Není problém na tohle místo zapsat jinou, menší hodnotu a tím způsobem zkrátit existující pole. Garbage collector v příštím cyklu uvidí kratší pole a zrecykluje nepotřebnou paměť.

Tento hack by byl skutečně užitečný a zjednodušil by některé algoritmy. Například když chci získat pole obsahující všechny společné prvky dvou seřazených polí, mám v současnosti dvě možnosti:

  • Alokuji buffer, ten částečně naplním a pak jeho obsah zkopíruji do pole příslušné délky.
  • Obě pole projdu, zjistím kolik obsahují společných prvků, alokuji pole potřebné délky a pak je projdu ještě jednou a budu plnit připravené pole.

Ani jeden z těchto případů není ideální. Mnohem efektivnější by bylo, kdybych alokoval pole maximální možné délky (v tomhle případě dlouhé jako menší z obou kolekcí), naplnil ho a pak zahodil zbytek pole. Používám přece managed runtime, sakra.

A o tom, že to je skutečně užitečné a nejde jenom o blouznění pomatených, svědčí fakt, že se přesně tohle plánuje do některé z dalších verzí Javy.

Hack zkracování pole by přesně tohle vyřešil. Bohužel nefunguje. Narazí na GC a „card marking“ stránek v paměti. Narazí tak tvrdě, že JVM segfaultne.

3) Nevyužité bajty v hlavičce

Ve hlavičce každého objektu na 64 bitových platformách je dohromady 24 nevyužitých bitů, které leží ladem a doslova čekají, až do nich něco zapíšu. Tahle data přežijí GC cyklus (logicky, GC kopíruje celý blok paměti objektu v jenom tažení). Situace s hlavičkami je celkem komplikovaná, ale stačí vědět, že jakmile přečtu identityHashCode objektu, je hlavička zafixována a volné bity zůstanou volné na věky věků.

Tento hack funguje jenom na 64 bit platformách (i s komprimovanými pointery) a jenom s některými garbage collectory.

4) Vlastní identity hashCode

Některé objekty (jako třeba String) si ukládají vlastní hashCode do soukromého atributu, aby ho nemusely neustále znovu a znovu počítat. Každý objekt má pak ještě nezávislý identityHashCode v hlavičce. Proč nevyužít prostor identityHashCode (který se stejně nikdy nepoužije) pro můj vlastní hashCode. V některých případech te může ušetřit až 8 bajtů na instanci (jeden int + padding).

Flattr this!

This entry was posted in Java, JVM, Paměť, Scala. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *