Duboko i pristupačno o Java magičnoj klasi Unsafe
Ranije, dok smo pričali o CAS-u i atomskim operacijama klase atomic, oba puta smo spomenuli Unsafe.
Unsafe je veoma posebna klasa u Javi — ona Javi pruža mehanizam nižeg nivoa, „nesiguran" mehanizam za direktan pristup i manipulaciju memorijom, nitima i objektima. Kao što ime sugeriše, Unsafe pruža mnogo nesigurnih operacija, pa ga treba koristiti sa velikim oprezom i samo u scenarijima koji zaista zahtevaju ovakve operacije nižeg nivoa.
Osnove Unsafe-a
Hajde prvo da pokušamo da dobijemo instancu Unsafe-a; ako pokušamo preko new, izvinite, kompajler će odmah prijaviti grešku:
Unsafe() has private access in 'sun.misc.Unsafe'Pregledom izvornog koda klase Unsafe se vidi da je ona obeležena sa final, pa ne dozvoljava nasleđivanje, a njen konstruktor je private, što znači da ne dozvoljava direktno instanciranje preko new. Ipak, u static bloku, Unsafe inicijalizuje jedan objekat Unsafe kao singleton:
public final class Unsafe {
private static final Unsafe theUnsafe;
...
private Unsafe() {
}
...
static {
theUnsafe = new Unsafe();
}
}Klasa Unsafe pruža statički metod getUnsafe koji na prvi pogled deluje kao da se njime može dobiti instanca Unsafe:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}Međutim, ako direktno pozovemo ovaj statički metod, takođe će se baciti izuzetak:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)Razlog je taj što u metodu getUnsafe postoji provera classLoader-a pozivaoca — proverava se da li je trenutna klasa učitana od strane Bootstrap classLoader-a; ako nije, baca se izuzetak SecurityException.
Drugim rečima, samo klase koje učita bootstrap class loader mogu pozivati metode klase Unsafe — to je zaštita da se ovi metodi ne bi pozivali iz nepoverljivog koda.
Zašto onda postoji ovako pažljivo ograničenje korišćenja klase Unsafe?
Kada se sve svede, razlog je u tome što su njene funkcije previše niskog nivoa — na primer direktna manipulacija memorijom, zaobilaženje JVM bezbednosnih provera pri kreiranju objekata i slično. Generalno, funkcije klase Unsafe se mogu podeliti na sledećih 8 kategorija:

Kreiranje instance
Videvši ove funkcije, sigurno vam se već žuri da ih isprobate?
Ako baš insistirate na pozivanju metoda klase Unsafe iz svog koda, kako onda dobiti njegovu instancu?
Odgovor je — iskoristiti refleksiju da preuzmete instancirani singleton objekat iz klase Unsafe:
public static Unsafe getUnsafe() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
//Field unsafeField = Unsafe.class.getDeclaredFields()[0]; // i ovako može, isti efekat
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
return unsafe;
}Nakon što preuzmete instancu Unsafe, možete sa njom radite šta god želite; hajde prvo da probamo čitanje i pisanje polja nekog objekta:
public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
User user=new User();
long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
System.out.println("offset:"+fieldOffset);
unsafe.putInt(user,fieldOffset,20);
System.out.println("age:"+unsafe.getInt(user,fieldOffset));
System.out.println("age:"+user.getAge());
}Pokretanjem koda dobijamo:
offset:12
age:20
age:20Vidimo da smo preko metode objectFieldOffset klase Unsafe dobili pomeraj (offset) polja unutar objekta; taj pomeraj nije apsolutna adresa u memoriji, već relativna. Zatim pomoću tog pomeraja vršimo čitanje i pisanje polja tipa int; iz rezultata se vidi da vrednosti dobijene preko Unsafe metoda i metoda get klase daju isti rezultat.
U gornjem primeru su pozvani metodi putInt i getInt klase Unsafe; pogledajmo njihove deklaracije u izvornom kodu:
public native int getInt(Object o, long offset);
public native void putInt(Object o, long offset, int x);Da objasnimo ulogu: getInt čita jedan int sa zadatog pomeraja unutar objekta, a putInt upisuje jedan int na zadati pomeraj — i to čak i ako je to polje u klasi private.
Pažljiviji čitaoci su možda primetili da ovi metodi, u poređenju sa običnim metodima koje inače pišemo, imaju dodatni modifikator native i nemaju konkretnu logiku — kako se onda implementiraju?
native metode
O native metodama smo već pričali; ovde ih ukratko podsetimo.
native metod, jednostavno rečeno, jeste interfejs kojim Java poziva ne-Java kod; pozvani metod je implementiran u ne-Java jeziku, na primer u C-u ili C++-u, kompajliran u DLL i zatim direktno pozvan iz Jave. native metode se pozivaju preko JNI (Java Native Interface); od Jave 1.1 JNI standard je deo Java platforme i omogućava Java kodu da interaguje sa kodom u drugim jezicima.

Mnogi osnovni metodi klase Unsafe su native metode; zašto uopšte koristiti native metode? Razlozi se svode na sledeće:
- Potreba za mogućnostima koje zavise od operativnog sistema, a koje Java ne poseduje; Java u ostvarenju platform-nezavisnosti istovremeno mora kontrolisati i niže nivoe, pa se oslanja na druge jezike.
- Neke funkcije su već gotove u drugim jezicima i Java ih može direktno pozvati.
- Kada je program osetljiv na vreme ili zahteva vrlo visoke performanse, nužno je koristiti jezik nižeg nivoa, kao što su C/C++, pa čak i asembler.
Mnoge konkurentne klase alata u paketu juc pri implementaciji konkurentnih mehanizama pozivaju native metode; preko native metoda se probija granica Java runtime-a i dolazi do određenih funkcija operativnog sistema nižeg nivoa.
Za jedan te isti native metod, različiti operativni sistemi mogu imati različite implementacije, ali je to za korisnika transparentno — konačan rezultat je uvek isti.
Primena Unsafe-a
Pošto smo stekli osnovno razumevanje Unsafe-a, pogledajmo njegovu osnovnu primenu.
1. Operacije nad memorijom
Ako ste pisali C ili C++, sigurno vam nije nepoznato upravljanje memorijom, dok Java ne dozvoljava direktne operacije nad memorijom — alokacija i oslobađanje memorije objekata je u potpunosti u domenu jvm-a. Ali u Unsafe-u, sledeći interfejsi omogućavaju direktne operacije nad memorijom:
//alokacija novog lokalnog memorijskog prostora
public native long allocateMemory(long bytes);
//ponovno podešavanje veličine memorijskog prostora
public native long reallocateMemory(long address, long bytes);
//postavlja memoriju na zadatu vrednost
public native void setMemory(Object o, long offset, long bytes, byte value);
//kopiranje memorije
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//oslobađanje memorije
public native void freeMemory(long address);Testirajmo sledećim kodom:
private void memoryTest() {
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr3 = unsafe.reallocateMemory(addr, size * 2);
System.out.println("addr: "+addr);
System.out.println("addr3: "+addr3);
try {
unsafe.setMemory(null,addr ,size,(byte)1);
for (int i = 0; i < 2; i++) {
unsafe.copyMemory(null,addr,null,addr3+size*i,4);
}
System.out.println(unsafe.getInt(addr));
System.out.println(unsafe.getLong(addr3));
}finally {
unsafe.freeMemory(addr);
unsafe.freeMemory(addr3);
}
}Prvo pogledajmo rezultat:
addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673Analizirajmo rezultat: prvo se metodom allocateMemory alocira memorijski prostor od 4 bajta; u petlji se metodom setMemory u svaki bajt upisuje vrednost 1 tipa bajt; kada se preko Unsafe pozove getInt, pošto jedna int promenljiva zauzima 4 bajta, čita se 4 bajta odjednom i formira se jedna int vrednost — odgovarajući decimalni rezultat je 16843009; proces se može razumeti preko crteža:

U kodu se metodom reallocateMemory ponovo alocira memorijski prostor od 8 bajtova; poređenjem addr i addr3 se vidi da je adresa drugačija od prethodno alocirane.
U drugoj for petlji, metoda copyMemory se poziva dvaput i vrši se kopiranje memorije; svaki put se kopira 4 bajta počevši od adrese addr, i to redom u memorijske prostore koji počinju od addr3 i addr3+4:

Nakon kopiranja, metodom getLong se odjednom čita 8 bajtova i dobija se long vrednost 72340172838076673.
Treba napomenuti da memorija alocirana na ovaj način spada u memoriju van hipa (off-heap) i ne podleže sakupljanju smeća — nju moramo posmatrati kao resurs i ručno je oslobađati metodom freeMemory, inače dolazi do curenja memorije.
Uobičajena praksa je da se operacije nad memorijom obavljaju u try bloku, a oslobađanje memorije u finally bloku.
2. Memorijske barijere
Pre nego što predstavimo memorijske barijere, treba znati da kompajler i CPU, uz garanciju konzistentnog rezultata programa, mogu preređivati instrukcije radi optimizacije performansi.
Preredba instrukcija može dovesti do nepoželjnog ishoda — nekonzistentnosti između CPU keša i podataka u memoriji, a memorijska barijera (Memory Barrier) upravo služi da spreči neispravne optimizacije kompajlera i hardvera tako što blokira preredbu instrukcija sa obe strane barijere.
Na hardverskom nivou, memorijska barijera je instrukcija koju pruža CPU da bi sprečio preredbu instrukcija; na različitim hardverskim platformama načini implementacije memorijske barijere mogu biti različiti.
U Javi 8 uvedena su tri metoda memorijskih barijera koji kriju razlike na nivou operativnog sistema i omogućavaju definisanje barijera u kodu, uz to da JVM jedinstveno generiše instrukcije memorijskih barijera. Unsafe pruža sledeća tri metoda vezana za memorijske barijere:
//zabranjuje preredbu operacija čitanja
public native void loadFence();
//zabranjuje preredbu operacija pisanja
public native void storeFence();
//zabranjuje preredbu i čitanja i pisanja
public native void fullFence();Memorijska barijera se može posmatrati kao sinhrona tačka u nasumičnim memorijskim operacijama — sve operacije čitanja i pisanja pre te tačke moraju biti završene pre nego što počnu operacije posle nje.
Uzmimo za primer metod loadFence: on zabranjuje preredbu operacija čitanja, osiguravajući da su sva čitanja pre barijere završena i da se keš podaci poništavaju i ponovo učitavaju iz glavne memorije.
Ovo će sigurno mnogima podsetiti na ključnu reč volatile: ako se polje obeleži sa volatile, može se postići vidljivost tog polja u višenitnom okruženju.
Na osnovu memorijske barijere za čitanje možemo ostvariti istu funkcionalnost. Definišimo nit-metodu koja menja indikator flag — pri čemu flag ovde nije obeležen sa volatile:
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}U while petlji glavne niti dodajmo memorijsku barijeru i testirajmo da li može da primeti promenu flag-a:
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); // dodata memorijska barijera za čitanje
if (flag){
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}Rezultat:
subThread change flag to:false
detected flag changed
main thread endAko uklonimo metod loadFence iz gornjeg koda, glavna nit neće moći da primeti promenu flag-a i ostace u while petlji zauvek. Proces ilustruje sledeća slika:

Čitaoci koji poznaju Java memorijski model (JMM) sigurno znaju da nit koja se izvršava ne čita promenljive direktno iz glavne memorije, već može operisati samo nad promenljivama u svojoj radnoj memoriji i zatim ih sinhronizovati sa glavnom memorijom; pri tome radna memorija niti se ne može deliti.
Tok na gornjoj slici je takav da podnit, preko glavne memorije, sinhronizuje izmenjeni rezultat glavnoj niti, čime menja radni prostor glavne niti i izlazi iz petlje.
3. Operacije nad objektima
01: Dobijanje memorijskog pomeraja polja objekta i izmena vrednosti tog polja smo već testirali u gornjem primeru.
Pored metoda putInt i getInt, Unsafe pruža put i get metode za svih 8 osnovnih tipova podataka i za Object, i svi put metodi mogu zaobići kontrolu pristupa i direktno menjati podatke u memoriji.
Čitanjem komentara u izvornom kodu openJDK-a možemo otkriti da se čitanje i pisanje osnovnih tipova i Object-a malo razlikuju — osnovni tipovi direktno operišu nad vrednošću (value), dok se operacije nad Object-om zasnivaju na referenci (reference value). Evo metoda za Object:
//na zadatom pomeraju objekta dohvata referencu na objekat
public native Object getObject(Object o, long offset);
//na zadatom pomeraju objekta upisuje referencu na objekat
public native void putObject(Object o, long offset, Object x);Pored običnog čitanja i pisanja polja, Unsafe pruža i volatile čitanje/pisanje i uređeno (ordered) upisivanje. volatile metodi čitanja/pisanja pokrivaju isti opseg kao i obični — sve osnovne tipove i Object; uzmimo za primer tip int:
//na zadatom pomeraju objekta čita int i podržava volatile load semantiku
public native int getIntVolatile(Object o, long offset);
//na zadatom pomeraju objekta upisuje int i podržava volatile store semantiku
public native void putIntVolatile(Object o, long offset, int x);U poređenju sa običnim čitanjem/pisanjem, volatile čitanje/pisanje ima veću cenu jer mora da garantuje vidljivost i uređenost. Pri get operaciji, vrednost polja se obavezno čita iz glavne memorije; pri put operaciji, vrednost se obavezno osvežava u glavnu memoriju, čime se osigurava da su te izmene vidljive drugim nitima.
Metodi uređenog upisivanja su sledeća tri:
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);Cena uređenog upisivanja je niža u poređenju sa volatile, jer garantuje samo uređenost prilikom upisa, a ne i vidljivost — dakle vrednost koju upiše jedna nit ne mora biti odmah vidljiva drugim nitima.
Da bismo razjasnili tu razliku, moramo produbiti znanje o memorijskim barijerama — prvo upoznajmo dve instrukcije:
Load: kopira podatke iz glavne memorije u keš procesoraStore: osvežava podatke iz keša procesora u glavnu memoriju
Razlika između uređenog i volatile upisa je u tipu memorijske barijere: kod uređenog upisa se koristi StoreStore barijera, a kod volatile upisa StoreLoad barijera, kao na slici ispod:

U metodi uređenog upisa koristi se StoreStore barijera koja osigurava da Store1 odmah osveži podatke u memoriju, i to pre Store2 i narednih instrukcija skladištenja.
U volatile upisu se koristi StoreLoad barijera koja osigurava da Store1 odmah osveži podatke u memoriju, i to pre Load2 i narednih instrukcija učitavanja; pri tome StoreLoad barijera zahteva da se sve instrukcije pristupa memoriji pre barijere — i skladištne i instrukcije pristupa — izvrše pre nego što počnu instrukcije pristupa memoriji posle barijere.
Ukratko, od tri vrste metoda upisa, po efikasnosti pisanja redosled je put, putOrder, putVolatile — opada.
02: Metodom allocateInstance klase Unsafe možemo instancirati objekte na nekonvencionalan način. Prvo definišimo klasu i u njenom konstruktoru dodelimo vrednosti njenim poljima:
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}Uporedimo kreiranje objekata preko konstruktora, refleksije i Unsafe metode:
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}Ispis je redom 1, 1, 0 — što pokazuje da pri kreiranju objekta preko allocateInstance ne poziva se konstruktor klase.
Pri ovakvom kreiranju objekta koristi se samo Class objekat, pa ako želimo da preskočimo fazu inicijalizacije objekta ili da zaobiđemo bezbednosne provere konstruktora, možemo iskoristiti ovaj pristup.
U gornjem primeru, ako konstruktor klase A proglasimo za private, nećemo moći da kreiramo objekat ni preko konstruktora ni preko refleksije, ali će allocateInstance i dalje raditi.
4. Operacije nad nizovima
U Unsafe-u se metodom arrayBaseOffset može dobiti pomeraj prvog elementa niza, a metodom arrayIndexScale inkrement pomeraja između elemenata niza. Testirajmo sledećim kodom:
private void arrayTest() {
String[] array=new String[]{"str1str1str","str2","str3"};
int baseOffset = unsafe.arrayBaseOffset(String[].class);
System.out.println(baseOffset);
int scale = unsafe.arrayIndexScale(String[].class);
System.out.println(scale);
for (int i = 0; i < array.length; i++) {
int offset=baseOffset+scale*i;
System.out.println(offset+" : "+unsafe.getObject(array,offset));
}
}Izlaz gornjeg koda je:
16
4
16 : str1str1str
20 : str2
24 : str3Kombinacijom početnog pomeraja niza i inkrementa pomeraja između elemenata možemo lako locirati poziciju elementa niza u memoriji, a zatim preko getObject direktno dobiti element niza na bilo kojoj poziciji.
Treba napomenuti da arrayIndexScale ne vraća veličinu elementa niza, već inkrement adrese; prema komentaru u openJDK-u, može se prevesti kao faktor skaliranja za adresiranje elemenata (scale factor for addressing elements).
U gornjem primeru prvi string je dugačak 11 bajtova, ali je inkrement adrese i dalje 4 bajta.
Kako se onda na osnovu ove dve vrednosti ostvaruje adresiranje i pristup elementima niza?
Nacrtajmo memorijski raspored String niza iz gornjeg primera da olakšamo razumevanje:

U objektu String niza, zaglavlje objekta se sastoji od tri dela: mark word zaurė reč zauzima 8 bajtova, klass point pokazivač tipa zauzima 4 bajta, a deo sa dužinom niza specifičan za nizove zauzima 4 bajta — ukupno 16 bajtova.
Pomeraj reference prvog String-a u odnosu na početnu adresu objekta je 16, a svaki sledeći element dodaje 4 — što odgovara procesu adresiranja u gornjem kodu; zatim se, preko metode getObject koju smo već spominjali, preko objekta niza dobija početna adresa u hipu, a kombinacijom sa pomerajima promenljivih unutar objekta dobijaju se reference svake promenljive.
5. CAS operacije
U konkurentnim alatkama paketa juc se obilno koristi CAS operacija — više puta smo je spominjali u člancima o synchronized i AQS-u; kao optimistična brava, CAS se široko primenjuje u konkurentnim alatkama.
Klasa Unsafe pruža metode compareAndSwapObject, compareAndSwapInt i compareAndSwapLong kojima se ostvaruje CAS operacija nad Object, int i long tipom. Uzmimo za primer compareAndSwapInt:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);U argumentima, o je objekat koji treba ažurirati, offset je pomeraj polja tipa int u objektu o; ako je vrednost tog polja jednaka expected, vrednost polja se postavlja na novu vrednost x, i to ažuriranje se ne može prekinuti — dakle atomična je operacija. Primer korišćenja compareAndSwapInt:
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}Pokretanjem koda redom se ispisuje:
1 2 3 4 5 6 7 8 9U gornjem primeru dve niti menjaju vrednost polja a tipa int, i to tako da se a menja na x samo ako je njena vrednost jednaka x-1, odnosno realizuje se uvećavanje a za jedan. Tok je prikazan ispod:

Treba napomenuti da nakon poziva compareAndSwapInt direktno dobijamo rezultat izmene true ili false, pa je potrebno ručno dodati logiku spinovanja u kod.
Klasa AtomicInteger u svom dizajnu takođe rezultat compareAndSwapInt koristi kao uslov petlje — vrti beskonačnu petlju sve dok izmena ne uspe, čime se ostvaruje atomično uvećanje.
6. Raspoređivanje niti
Klasa Unsafe pruža metode park, unpark, monitorEnter, monitorExit i tryMonitorEnter za raspoređivanje niti; u članku o AQS-u smo već pomenuli korišćenje LockSupport-a za suspendovanje ili buđenje zadate niti. O toj klasi smo već govorili, pa je ovde podsećamo.
Pogledajmo izvorni kod LockSupport-a — vidimo da i on poziva metode klase Unsafe:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}Metod park klase LockSupport poziva metod park klase Unsafe da blokira trenutnu nit; nakon što se nit blokira, ne nastavlja izvršenje sve dok druga nit ne pozove unpark da je probudi. Sledeći primer testira ova dva metoda klase Unsafe:
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false,0L);
System.out.println("unpark mainThread success");
}Program ispisuje:
park main mainThread
subThread try to unpark mainThread
unpark mainThread successTok programa je prilično lako razumeti: podnit nakon pokretanja prvo ide na spavanje, čime se osigurava da glavna nit stigne da pozove park i blokira sebe; nakon 5 sekundi podnit poziva unpark i budi glavnu nit, čime glavna nit nastavlja dalje. Ceo tok je na slici ispod:

Pored toga, tri metoda vezana za monitor u izvornom kodu Unsafe-a su označena kao deprecated i ne preporučuje im se upotreba:
//acquire-uje bravu nad objektom
@Deprecated
public native void monitorEnter(Object var1);
//oslobađa bravu nad objektom
@Deprecated
public native void monitorExit(Object var1);
//pokušava da acquire-uje bravu nad objektom
@Deprecated
public native boolean tryMonitorEnter(Object var1);Metod monitorEnter acquire-uje bravu nad objektom, monitorExit oslobađa bravu nad objektom — ako se ovaj metod pozove nad objektom koji nije zaključan sa monitorEnter, biće bačen izuzetak IllegalMonitorStateException. Metod tryMonitorEnter pokušava da acquire-uje bravu nad objektom; ako uspe vraća true, inače false.
7. Operacije nad Class-om
Operacije Unsafe-a nad Class-om obuhvataju uglavnom učitavanje klasa i operacije nad statičkim promenljivama.
01: Metodi za čitanje statičkih polja:
//vraća pomeraj statičkog polja
public native long staticFieldOffset(Field f);
//vraća pokazivač na objekat statičkog polja
public native Object staticFieldBase(Field f);
//proverava da li klasu treba instancirati (provera pre preuzimanja statičkih polja)
public native boolean shouldBeInitialized(Class<?> c);Kreirajmo klasu sa statičkim poljem i testirajmo:
@Data
public class User {
public static String name="Hydra";
int age;
}
private void staticTest() throws Exception {
User user=new User();
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}Rezultat:
false
HydraU operacijama nad objektima učili smo da pomoću metode objectFieldOffset dobijemo pomeraj polja objekta i na osnovu njega pristupamo vrednosti; međutim, taj pristup ne važi za statička polja klase — za to koristimo staticFieldOffset.
U gornjem kodu, za preuzimanje Field objekta potrebno je osloniti se na Class, dok za preuzimanje statičkih polja više ne zavismo od Class-a.
U gornjem kodu prvo se kreira User objekat, jer ako klasa nije instancirana, njena statička polja se neće inicijalizovati, pa bi preuzeto polje bilo null. Zato je pre preuzimanja statičkog polja potrebno pozvati shouldBeInitialized da proverimo da li klasu treba inicijalizovati. Ako uklonimo liniju koja kreira User objekat, rezultat postaje:
true
null02: Metod defineClass omogućava programu da dinamički kreira klasu u toku izvršenja; definicija metoda:
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,ProtectionDomain protectionDomain);U praksi se mogu proslediti samo niz bajtova, indeks početnog bajta i broj bajtova za čitanje; podrazumevano, class loader (ClassLoader) i zaštitni domen (ProtectionDomain) potiču od instance koja poziva ovaj metod. Sledeći primer realizuje funkcionalnost učitavanja dekompajlirane class datoteke:
private static void defineTest() {
String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}U gornjem kodu prvo se učita jedna class datoteka i pretvori u niz bajtova preko datotočnog toka; zatim se metodom defineClass dinamički kreira klasa, nakon čega sledi njena instancijacija; tok je prikazan na slici ispod, i pri tome klasa kreirana na ovaj način zaobilazi sve JVM bezbednosne provere.

Pored metoda defineClass, Unsafe pruža i metod defineAnonymousClass:
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);Ovaj metod dinamički kreira anonimnu klasu; Lambda izrazi koriste ASM za dinamičko generisanje bajtkoda, a zatim ovaj metod za definisanje anonimne klase koja implementira odgovarajući funkcionalni interfejs.
U novim mogućnostima objavljenim u JDK 15, pod stavkom o skrivenim klasama (Hidden classes), najavljeno je da će u budućim verzijama biti napušten metod defineAnonymousClass klase Unsafe.
8. Informacije o sistemu
Metode addressSize i pageSize klase Unsafe služe za dobijanje informacija o sistemu; metod addressSize vraća veličinu sistemskog pokazivača — na 64-bitnom sistemu podrazumevano vraća 8, a na 32-bitnom 4. Metod pageSize vraća veličinu memorijske stranice, koja je stepen dvojke. Ispišimo to direktno sledećim kodom:
private void systemTest() {
System.out.println(unsafe.addressSize());
System.out.println(unsafe.pageSize());
}Rezultat:
8
4096Ova dva metoda imaju relativno malo primena; u klasi java.nio.Bits, metod pageCount pri izračunavanju potrebnog broja memorijskih stranica poziva pageSize da dobije veličinu stranice. Pored toga, metod copySwapMemory pri kopiranju memorije poziva addressSize da detektuje 32-bitni sistem.
Kratak pregled
U ovom članku smo prvo predstavili osnovne koncepte i principe rada Unsafe-a, a na toj osnovi objasnili i demonstrirali njegov API.
Verujemo da se kroz ovaj proces može uvideti da Unsafe u određenim scenarijima zaista može doneti programersku pogodnost. Ali uz te pogodnosti idu i određeni bezbednosni rizici; po mom mišljenju, to što neka tehnologija nosi nesigurnost nije strašno — strašno je kada se u upotrebi zloupotrebi.
Iako su kružile glasine da će klasa Unsafe biti uklonjena u Javi 9, ona je ipak opstala sve do JDK 16; prema logici „postojećem mesto je opravdano", uz pravilnu upotrebu može nam doneti mnogo koristi, pa na kraju ipak preporučujemo da pri korišćenju Unsafe-a budite obazrivi i da izbegavate zloupotrebu.
Autor: Chenmo Wang Er. Sadržaj pre uređivanja potiče uglavnom iz članka prijatelja „Ma Nong Can Shang" na WeChat javnom nalogu.
