主頁 > 後端開發 > Java 中的 Unsafe 魔法類,到底有啥用?

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

2021-06-18 06:14:48 後端開發

作者: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開發手冊(嵩山版)》最新發布,速速下載!

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288051.html

標籤:其他

上一篇:閱讀原始碼很重要,以logback為例,分享一個小白都能學會的讀原始碼方法

下一篇:springcloud 2020 gateway 503 錯誤代碼

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more