Java 中的 Unsafe 魔法類,到底有啥用?

作者:rickiyang

出處:www.cnblogs.com/rickiyang/p/11334887.html

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增強Java語言底層資源操作能力方面起到了很大的作用。

但是,這個類的作者不希望我們使用它,因爲我們雖然我們獲取到了對底層的控制權,但是也增大了風險,安全性正是Java相對於C++/C的優勢。因爲該類在sun.misc包下,默認是被BootstrapClassLoader加載的。如果我們在程序中去調用這個類的話,我們使用的類加載器肯定是 AppClassLoader,問題是在Unsafe中是這樣寫的:

private static final Unsafe theUnsafe;

private Unsafe() {
}

@CallerSensitive
public static Unsafe getUnsafe() {
  Class var0 = Reflection.getCallerClass();
  if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
    throw new SecurityException("Unsafe");
  } else {
    return theUnsafe;
  }
}

將構造函數私有,然後提供了一個靜態方法去獲取當前類實例。在getUnsafe()方法中首先判斷當前類加載器是否爲空,因爲使用 BootstrapClassLoader 本身就是空,它是用c++實現的,這樣就限制了我們在自己的代碼中使用這個類。

但是同時作者也算是給我們提供了一個後門,因爲Java有反射機制。調用的思路就是將theUnsafe對象設置爲可見。

Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
System.out.println(unsafe);

unsafe類功能介紹:

img

內存操作

這部分主要包含堆外內存的分配、拷貝、釋放、給定地址值操作等方法。

//分配內存, 相當於C++的malloc函數
public native long allocateMemory(long bytes);
//擴充內存
public native long reallocateMemory(long address, long bytes);
//釋放內存
public native void freeMemory(long address);
//在給定的內存塊中設置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//內存拷貝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//獲取給定地址值,忽略修飾限定符的訪問限制。與此類似操作還有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//爲給定地址設置值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//獲取給定地址的byte類型的值(當且僅當該內存地址爲allocateMemory分配時,此方法結果爲確定的)
public native byte getByte(long address);
//爲給定地址設置byte類型的值(當且僅當該內存地址爲allocateMemory分配時,此方法結果纔是確定的)
public native void putByte(long address, byte x);

通常,我們在Java中創建的對象都處於堆內內存(heap)中,堆內內存是由JVM所管控的Java進程內存,並且它們遵循JVM的內存管理機制,JVM會採用垃圾回收機制統一管理堆內存。與之相對的是堆外內存,存在於JVM管控之外的內存區域,Java中對堆外內存的操作,依賴於Unsafe提供的操作堆外內存的native方法。

使用堆外內存的原因

  • 對垃圾回收停頓的改善。由於堆外內存是直接受操作系統管理而不是JVM,所以當我們使用堆外內存時,即可保持較小的堆內內存規模。從而在GC時減少回收停頓對於應用的影響。
  • 提升程序I/O操作的性能。通常在I/O通信過程中,會存在堆內內存到堆外內存的數據拷貝操作,對於需要頻繁進行內存間數據拷貝且生命週期較短的暫存數據,都建議存儲到堆外內存。

典型應用

DirectByteBuffer是Java用於實現堆外內存的一個重要類,通常用在通信過程中做緩衝池,如在Netty、MINA等NIO框架中應用廣泛。DirectByteBuffer對於堆外內存的創建、使用、銷燬等邏輯均由Unsafe提供的堆外內存API來實現。

下面的代碼爲DirectByteBuffer構造函數,創建DirectByteBuffer的時候,通過Unsafe.allocateMemory分配內存、Unsafe.setMemory進行內存初始化,而後構建Cleaner對象用於跟蹤DirectByteBuffer對象的垃圾回收,以實現當DirectByteBuffer被垃圾回收時,分配的堆外內存一起被釋放。

DirectByteBuffer(int cap) {                   // package-private

  super(-1, 0, cap, cap);
  boolean pa = VM.isDirectMemoryPageAligned();
  int ps = Bits.pageSize();
  long size = Math.max(1L, (long)cap + (pa ? ps : 0));
  Bits.reserveMemory(size, cap);

  long base = 0;
  try {
    //分配內存,返回基地址
    base = unsafe.allocateMemory(size);
  } catch (OutOfMemoryError x) {
    Bits.unreserveMemory(size, cap);
    throw x;
  }
  //內存初始化
  unsafe.setMemory(base, size, (byte) 0);
  if (pa && (base % ps != 0)) {
    // Round up to page boundary
    address = base + ps - (base & (ps - 1));
  } else {
    address = base;
  }
  //跟蹤directbytebuffer 對象的垃圾回收,實現堆外內存的釋放
  cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  att = null;



}

上面最後一句代碼通過Cleaner.create()來進行對象監控,釋放堆外內存。這裏是如何做到的呢?跟蹤一下Cleaner類:

public class Cleaner extends PhantomReference<Object> {
  
   public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
}

可以看到繼承了PhantomReference,Java中的4大引用類型我們都知道。PhantomReference的作用於其他的Refenrence作用大有不同。像 SoftReference、WeakReference都是爲了保證引用的類對象能在不用的時候及時的被回收,但是 PhantomReference 並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,對象不可達時就會被垃圾回收器回收,但是任何時候都無法通過虛引用獲得對象。虛引用主要用來跟蹤對象被垃圾回收器回收的活動。

那他的作用到底是啥呢?準確來說 PhantomReference 給使用者提供了一種機制-來監控對象的垃圾回收的活動。

可能這樣說不是太明白,我來舉個例子:

package com.rickiyang.learn.javaagent;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;


/**
 * @author rickiyang
 * @date 2019-08-08
 * @Desc
 */
public class TestPhantomReference {
  public static boolean isRun = true;

  public static void main(String[] args) throws Exception {
    String str = new String("123");
    System.out.println(str.getClass() + "@" + str.hashCode());
    final ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
    new Thread(() -> {
      while (isRun) {
        Object obj = referenceQueue.poll();
        if (obj != null) {
          try {
            Field rereferent = Reference.class.getDeclaredField("referent");
            rereferent.setAccessible(true);
            Object result = rereferent.get(obj);
            System.out.println("gc will collect:"
                               + result.getClass() + "@"
                               + result.hashCode() + "\t"
                               + result);
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }
    }).start();
    PhantomReference<String> weakRef = new PhantomReference<>(str, referenceQueue);
    str = null;
    Thread.currentThread().sleep(2000);
    System.gc();
    Thread.currentThread().sleep(2000);
    isRun = false;
  }
}

上面這段代碼的含義是new PhantomReference(),因爲PhantomReference必須的維護一個ReferenceQueue用來保存當前被虛引用的對象。上例中手動去調用referenceQueue.poll()方法,這裏你需要注意的是並不是我們主動去釋放queue中的對象,你跟蹤進去 poll() 方法可以看到有一個全局鎖對象,只有噹噹前對象失去了引用之後纔會釋放鎖,poll()方法才能執行。在執行poll()方法釋放對象的時候我們可以針對這個對象做一些監控。這就是 PhantomReference 的意義所在。

說回到 Cleaner, 通過看源碼,create()方法調用了add()方法,在Cleaner類裏面維護了一個雙向鏈表,將每一個add進來的Cleaner對象都添加到這個鏈表中維護。那麼在Cleaner 鏈表中的對象實在何時被釋放掉呢?

注意到 Cleaner中有一個clean()方法:

public void clean() {
  if (remove(this)) {
    try {
      this.thunk.run();
    } catch (final Throwable var2) {
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
          if (System.err != null) {
            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
          }

          System.exit(1);
          return null;
        }
      });
    }

  }
}

remove()方法是將該對象從內部維護的雙向鏈表中清除。下面緊跟着是thunk.run() ,thunk = 我們通過create()方法傳進來的參數,在``DirectByteBuffer中那就是:Cleaner.create(this, new Deallocator(base, size, cap))`,Deallocator類也是一個線程:

private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

       //省略無關 代碼
        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

看到在run方法中調用了freeMemory()去釋放掉對象。

Reference類中調用了該方法,Reference 類中的靜態代碼塊 有個一內部類:ReferenceHandler,它繼承了 Thread,在run方法中調用了 tryHandlePending(),並且被設置爲守護線程,意味着會循環不斷的處理pending鏈表中的對象引用。

這裏要注意的點是:

Cleaner本身不帶有清理邏輯,所有的邏輯都封裝在thunk中,因此thunk是怎麼實現的纔是最關鍵的。

另外,Java 最新核心技術系列教程和示例源碼看這裏:https://github.com/javastacks/javastack

static {
  ThreadGroup tg = Thread.currentThread().getThreadGroup();
  for (ThreadGroup tgn = tg;
       tgn != null;
       tg = tgn, tgn = tg.getParent());
  Thread handler = new ReferenceHandler(tg, "Reference Handler");
  /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
  handler.setPriority(Thread.MAX_PRIORITY);
  handler.setDaemon(true);
  handler.start();

  // provide access in SharedSecrets
  SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
    @Override
    public boolean tryHandlePendingReference() {
      return tryHandlePending(false);
    }
  });
}


static boolean tryHandlePending(boolean waitForNotify) {
  Reference<Object> r;
  Cleaner c;
  try {
    synchronized (lock) {
      if (pending != null) {
        r = pending;
        //如果當前Reference對象是Cleaner類型的就進行特殊處理
        c = r instanceof Cleaner ? (Cleaner) r : null;
        // unlink 'r' from 'pending' chain
        pending = r.discovered;
        r.discovered = null;
      } else {
        // The waiting on the lock may cause an OutOfMemoryError
        // because it may try to allocate exception objects.
        if (waitForNotify) {
          lock.wait();
        }
        // retry if waited
        return waitForNotify;
      }
    }
  } catch (OutOfMemoryError x) {
    Thread.yield();
    // retry
    return true;
  } catch (InterruptedException x) {
    // retry
    return true;
  }

  // clean 不爲空的時候,走清理的邏輯
  if (c != null) {
    c.clean();
    return true;
  }

  ReferenceQueue<? super Object> q = r.queue;
  if (q != ReferenceQueue.NULL) q.enqueue(r);
  return true;
}

tryHandlePending這段代碼的意思是:

如果一個對象經過JVM檢測他已經沒有強引用了,但是還有 弱引用 或者 軟引用 或者 虛引用的情況下,那麼就會把此對象放到一個名爲pending的鏈表裏,這個鏈表是通過Reference.discovered域連接在一起的。

ReferenceHandler這個線程會一直從鏈表中取出被pending的對象,它可能是WeakReference,也可能是SoftReference,當然也可能是PhantomReference和Cleaner。如果是Cleaner,那就直接調用Cleaner的clean方法,然後就結束了。其他的情況下,要交給這個對象所關聯的queue,以便於後續的處理。

關於堆外內存分配和回收的代碼我們就先分析到這裏。需要注意的是對外內存回收的時機也是不確定的,所以不要持續分配一些大對象到堆外,如果沒有被回收掉,這是一件很可怕的事情。畢竟它無法被JVM檢測到。

內存屏障

硬件層的內存屏障分爲兩種:Load BarrierStore Barrier即讀屏障和寫屏障。內存屏障有兩個作用:阻止屏障兩側的指令重排序;強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效。在Unsafe中提供了三個方法來操作內存屏障:

//讀屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障後,屏障後的load操作不能被重排序到屏障前
public native void loadFence();
//寫屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障後,屏障後的store操作不能被重排序到屏障前
public native void storeFence();
//全能屏障,禁止load、store操作重排序
public native void fullFence();

先簡單瞭解兩個指令:

  • Store:將處理器緩存的數據刷新到內存中。
  • Load:將內存存儲的數據拷貝到處理器的緩存中。

JVM平臺提供了一下幾種內存屏障:

屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 該屏障確保Load1數據的裝載先於Load2及其後所有裝載指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 該屏障確保Store1立刻刷新數據到內存(使其對其他處理器可見)該操作先於Store2及其後所有存儲指令的操作
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的數據裝載先於Store2及其後所有的存儲指令刷新數據到內存的操作
StoreLoad Barriers Store1;StoreLoad;Load2 該屏障確保Store1立刻刷新數據到內存的操作先於Load2及其後所有裝載裝載指令的操作。它會使該屏障之前的所有內存訪問指令(存儲指令和訪問指令)完成之後,才執行該屏障之後的內存訪問指令

StoreLoad Barriers同時具備其他三個屏障的效果,因此也稱之爲全能屏障(mfence),是目前大多數處理器所支持的;但是相對其他屏障,該屏障的開銷相對昂貴。

loadFence

實現了LoadLoad Barriers,該操作禁止了指令的重排序。

storeFence

實現了 StoreStore Barriers,確保屏障前的寫操作能夠立刻刷入到主內存,並且確保屏障前的寫操作一定先於屏障後的寫操作。即保證了內存可見性和禁止指令重排序。

fullFence

實現了 StoreLoad Barriers,強制所有在mfence指令之前的store/load指令,都在該mfence指令執行之前被執行;所有在mfence指令之後的store/load指令,都在該mfence指令執行之後被執行。

在 JDK 中調用了 內存屏障這幾個方法的實現類有 StampedLock。關於StampedLock的實現我們後面會專門抽出一篇去講解。它並沒有去實現AQS隊列。而是採用了 其他方式實現。

系統相關

這部分包含兩個獲取系統相關信息的方法。

//返回系統指針的大小。返回值爲4(32位系統)或 8(64位系統)。
public native int addressSize();  
//內存頁的大小,此值爲2的冪次方。
public native int pageSize();

java.nio下的Bits類中調用了pagesize()方法計算系統中頁大小:

private static int pageSize = -1;

static int pageSize() {
    if (pageSize == -1)
        pageSize = unsafe().pageSize();
    return pageSize;
}

線程調度

線程調度中提供的方法包括:線程的掛起,恢復 和 對象鎖機制等,其中獲取對象的監視器鎖方法已經被標記爲棄用。

// 終止掛起的線程,恢復正常.java.util.concurrent包中掛起操作都是在LockSupport類實現的,其底層正是使用這兩個方法
public native void unpark(Object thread);
// 線程調用該方法,線程將一直阻塞直到超時,或者是中斷條件出現。
public native void park(boolean isAbsolute, long time);
//獲得對象鎖(可重入鎖)
@Deprecated
public native void monitorEnter(Object o);
//釋放對象鎖
@Deprecated
public native void monitorExit(Object o);
//嘗試獲取對象鎖
@Deprecated
public native boolean tryMonitorEnter(Object o);

將一個線程進行掛起是通過 park 方法實現的,調用park()後,線程將一直 阻塞 直到 超時 或者 中斷 等條件出現。unpark可以釋放一個被掛起的線程,使其恢復正常。整個併發框架中對線程的掛起操作被封裝在LockSupport類中,LockSupport 類中有各種版本 pack 方法,但最終都調用了Unsafe.park()方法。 我們來看一個例子:

package leetcode;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

/**
 * @author: rickiyang
 * @date: 2019/8/10
 * @description:
 */
public class TestUsafe {

    private static Thread mainThread;


    public Unsafe getUnsafe() throws Exception {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        return (Unsafe) theUnsafeField.get(null);
    }

    public void testPark() throws Exception {
        Unsafe unsafe = getUnsafe();
        mainThread = Thread.currentThread();

        System.out.println(String.format("park %s", mainThread.getName()));
        unsafe.park(false, TimeUnit.SECONDS.toNanos(3));

        new Thread(() -> {
            System.out.println(String.format("%s unpark %s", Thread.currentThread().getName(),
                                             mainThread.getName()));
            unsafe.unpark(mainThread);
        }).start();
        System.out.println("main thread is done");

    }

    public static void main(String[] args) throws Exception {
        TestUsafe testUsafe = new TestUsafe();
        testUsafe.testPark();
    }

}

運行上面的例子,那你會發現在第29行 park方法設置了超時時間爲3秒後,會阻塞當前主線程,直到超時時間到達,下面的代碼纔會繼續執行。

對象操作

Unsafe類中提供了多個方法來進行 對象實例化 和 獲取對象的偏移地址 的操作:

// 傳入一個Class對象並創建該實例對象,但不會調用構造方法
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

// 獲取字段f在實例對象中的偏移量
public native long objectFieldOffset(Field f);

// 返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
// 靜態屬性的偏移量,用於在對應的Class對象中讀寫靜態屬性
public native long staticFieldOffset(Field f);

// 獲得給定對象偏移量上的int值,所謂的偏移量可以簡單理解爲指針指向該變量;的內存地址,
// 通過偏移量便可得到該對象的變量,進行各種操作
public native int getInt(Object o, long offset);
// 設置給定對象上偏移量的int值
public native void putInt(Object o, long offset, int x);

// 獲得給定對象偏移量上的引用類型的值
public native Object getObject(Object o, long offset);
// 設置給定對象偏移量上的引用類型的值
public native void putObject(Object o, long offset, Object x););

// 設置給定對象的int值,使用volatile語義,即設置後立馬更新到內存對其他線程可見
public native void putIntVolatile(Object o, long offset, int x);
// 獲得給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。
public native int getIntVolatile(Object o, long offset);

// 與putIntVolatile一樣,但要求被操作字段必須有volatile修飾
public native void putOrderedInt(Object o, long offset, int x);

allocateInstance方法在這幾個場景下很有用:跳過對象的實例化階段(通過構造函數)、忽略構造函數的安全檢查(反射newInstance()時)、你需要某類的實例但該類沒有public的構造函數。

另外,Java 最新核心技術系列教程和示例源碼看這裏:https://github.com/javastacks/javastack

舉個例子:

public class User {

    private String name;
    private int age;
    private static String address = "beijing";

    public User(){
        name = "xiaoming";
    }

    public String getname(){
        return name;
    }
}

	/**
     * 實例化對象
     * @throws Exception
     */
public void newInstance() throws Exception{
    TestUsafe testUsafe = new TestUsafe();
    Unsafe unsafe = testUsafe.getUnsafe();
    User user = new User();
    System.out.println(user.getname());

    User user1 = User.class.newInstance();
    System.out.println(user1.getname());

    User o = (User)unsafe.allocateInstance(User.class);
    System.out.println(o.getname());
}

打印的結果可以看到最後輸出的是null,說明構造函數未被加載。可以進一步實驗,將User類中的構造函數設置爲 private,你會發現在前面兩種實例化方式檢查期就報錯。但是第三種是可以用的。這是因爲allocateInstance只是給對象分配了內存,它並不會初始化對象中的屬性。

下面是對象操作的使用示例:

public void testObject() throws Exception{
    TestUsafe testUsafe = new TestUsafe();
    Unsafe unsafe = testUsafe.getUnsafe();

    //通過allocateInstance創建對象,爲其分配內存地址,不會加載構造函數
    User user = (User) unsafe.allocateInstance(User.class);
    System.out.println(user);

    // Class && Field
    Class<? extends User> userClass = user.getClass();
    Field name = userClass.getDeclaredField("name");
    Field age = userClass.getDeclaredField("age");
    Field location = userClass.getDeclaredField("address");

    // 獲取實例域name和age在對象內存中的偏移量並設置值
    System.out.println(unsafe.objectFieldOffset(name));
    unsafe.putObject(user, unsafe.objectFieldOffset(name), "xiaoming");
    System.out.println(unsafe.objectFieldOffset(age));
    unsafe.putInt(user, unsafe.objectFieldOffset(age), 18);
    System.out.println(user);

    // 獲取定義location字段的類
    Object staticFieldBase = unsafe.staticFieldBase(location);
    System.out.println(staticFieldBase);

    // 獲取static變量address的偏移量
    long staticFieldOffset = unsafe.staticFieldOffset(location);
    // 獲取static變量address的值
    System.out.println(unsafe.getObject(staticFieldBase, staticFieldOffset));
    // 設置static變量address的值
    unsafe.putObject(staticFieldBase, staticFieldOffset, "tianjin");
    System.out.println(user + " " + user.getAddress());
}

對象實例佈局與內存大小

一個Java對象佔用多大的內存空間呢?這個問題很值得讀者朋友去查一下。 因爲這個輸出本篇的重點所以簡單說一下。一個 Java 對象在內存中由對象頭、示例數據和對齊填充構成。對象頭存儲了對象運行時的基本數據,如 hashCode、鎖狀態、GC 分代年齡、類型指針等等。實例數據是對象中的非靜態字段值,可能是一個原始類型的值,也可能是一個指向其他對象的指針。對齊填充就是 padding,保證對象都採用 8 字節對齊。除此以外,在 64 位虛擬機中還可能會開啓指針壓縮,將 8 字節的指針壓縮爲 4 字節,這裏就不再過多介紹了。

也就是說一個 Java 對象在內存中,首先是對象頭,然後是各個類中字段的排列,這之間可能會有 padding 填充。這樣我們大概就能理解字段偏移量的含義了,它實際就是每個字段在內存中所處的位置。

public class User {

    private String name;
    private int age;
}

TestUsafe testUsafe = new TestUsafe();
Unsafe unsafe = testUsafe.getUnsafe();

for (Field field : User.class.getDeclaredFields()) {
    System.out.println(field.getName() + "-" + field.getType() + ": " + unsafe.objectFieldOffset(field));
}

結果:
name-class java.lang.String: 16
age-int: 12

從上面的運行結果中可以:
age:偏移值爲12,即前面 12 個字節的對象頭;

name:name從16字節開始,因爲int 類型的age佔了4個字節。

繼續算下去整個對象佔用的空間,對象頭12,age 4,name 是指針類型,開啓指針壓縮佔用4個字節,那麼User對象整個佔用20字節,因爲上面說的padding填充,必須8字節對齊,那麼實際上會補上4個字節的填充,即一共佔用了24個字節。

按照這種計算方式,我們可以字節寫一個計算size的工具類:

public static long sizeOf(Object o) throws Exception{
    TestUsafe testUsafe = new TestUsafe();
    Unsafe unsafe = testUsafe.getUnsafe();
    HashSet<Field> fields = new HashSet<Field>();
    Class c = o.getClass();
    while (c != Object.class) {
        for (Field f : c.getDeclaredFields()) {
            if ((f.getModifiers() & Modifier.STATIC) == 0) {
                fields.add(f);
            }
        }
        //如果有繼承父類的話,父類中的屬性也是要計算的
        c = c.getSuperclass();
    }
    //計算每個字段的偏移量,因爲第一個字段的偏移量即在對象頭的基礎上偏移的
    //所以只需要比較當前偏移量最大的字段即表示這是該對象最後一個字段的位置
    long maxSize = 0;
    for (Field f : fields) {
        long offset = unsafe.objectFieldOffset(f);
        if (offset > maxSize) {
            maxSize = offset;
        }
    }
    //上面計算的是對象最後一個字段的偏移量起始位置,java中對象最大長度是8個字節(long)
    //這裏的計算方式是 將 當前偏移量 / 8 + 8字節 的padding
    return ((maxSize/8) + 1) * 8;
}

上面的工具類計算的結果也是24。

class相關操作

//靜態屬性的偏移量,用於在對應的Class對象中讀寫靜態屬性
public native long staticFieldOffset(Field f);
//獲取一個靜態字段的對象指針
public native Object staticFieldBase(Field f);
//判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候(因爲一個類如果沒初始化,它的靜態屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false
public native boolean shouldBeInitialized(Class<?> c);
//確保類被初始化
public native void ensureClassInitialized(Class<?> c);
//定義一個類,可用於動態創建類,此方法會跳過JVM的所有安全檢查,默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例來源於調用者
public native Class<?> defineClass(String name, byte[] b, int off, int len,
                                   ClassLoader loader,
                                   ProtectionDomain protectionDomain);
//定義一個匿名類,可用於動態創建類
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

數組操作

數組操作主要有兩個方法:

//返回數組中第一個元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回數組中一個元素佔用的大小
public native int arrayIndexScale(Class<?> arrayClass);

CAS操作

相信所有的開發者對這個詞都不陌生,在AQS類中使用了無鎖的方式來進行併發控制,主要就是CAS的功勞。

CAS的全稱是Compare And Swap 即比較交換,其算法核心思想如下

執行函數:CAS(V,E,N)

包含3個參數

  1. V表示要更新的變量
  2. E表示預期值
  3. N表示新值

如果V值等於E值,則將V的值設爲N。若V值和E值不同,則說明已經有其他線程做了更新,則當前線程什麼都不做。通俗的理解就是CAS操作需要我們提供一個期望值,當期望值與當前線程的變量值相同時,說明沒有別的線程修改該值,當前線程可以進行修改,也就是執行CAS操作,但如果期望值與當前線程不符,則說明該值已被其他線程修改,此時不執行更新操作,但可以選擇重新讀取該變量再嘗試再次修改該變量,也可以放棄操作。

Unsafe類中提供了三個方法來進行CAS操作:

public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
  
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

另外,在 JDK1.8中新增了幾個 CAS 的方法,他們的實現是基於上面三個方法做的一層封裝:

 //1.8新增,給定對象o,根據獲取內存偏移量指向的字段,將其增加delta,
 //這是一個CAS操作過程,直到設置成功方能退出循環,返回舊值
 public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //獲取內存中最新值
         v = getIntVolatile(o, offset);
       //通過CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

//1.8新增,方法作用同上,只不過這裏操作的long類型數據
 public final long getAndAddLong(Object o, long offset, long delta) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, v + delta));
     return v;
 }

 //1.8新增,給定對象o,根據獲取內存偏移量對於字段,將其 設置爲新值newValue,
 //這是一個CAS操作過程,直到設置成功方能退出循環,返回舊值
 public final int getAndSetInt(Object o, long offset, int newValue) {
     int v;
     do {
         v = getIntVolatile(o, offset);
     } while (!compareAndSwapInt(o, offset, v, newValue));
     return v;
 }

// 1.8新增,同上,操作的是long類型
 public final long getAndSetLong(Object o, long offset, long newValue) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, newValue));
     return v;
 }

 //1.8新增,同上,操作的是引用類型數據
 public final Object getAndSetObject(Object o, long offset, Object newValue) {
     Object v;
     do {
         v = getObjectVolatile(o, offset);
     } while (!compareAndSwapObject(o, offset, v, newValue));
     return v;
 }

CAS在java.util.concurrent.atomic相關類、Java AQS、CurrentHashMap等實現上有非常廣泛的應用。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!

3.阿里 Mock 工具正式開源,幹掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式發佈,全新顛覆性版本!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章