Unsafe

N1ce2cu發表於2024-07-27

Unsafe 基礎


Unsafe 是 Java 中一個非常特殊的類,它為 Java 提供了一種底層、"不安全"的機制來直接訪問和操作記憶體、執行緒和物件。正如其名字所暗示的,Unsafe 提供了許多不安全的操作,因此它的使用應該非常小心,並限於那些確實需要使用這些底層操作的場景。

Unsafe 在 static 靜態程式碼塊中,以單例的方式初始化了一個 Unsafe 物件:

public final class Unsafe {
     private static final Unsafe theUnsafe;
     ...
     private Unsafe() {
     }
     ...
     static {
         theUnsafe = new Unsafe();
     }   
 }

Unsafe 類提供了一個靜態方法getUnsafe,看上去貌似可以用它來獲取 Unsafe 例項:

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

直接呼叫這個靜態方法,會丟擲 SecurityException 異常:

Exception in thread "main" java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at org.example.Main.main(Main.java:7)

這是因為在getUnsafe方法中,會對呼叫者的classLoader進行檢查,判斷當前類是否由Bootstrap classLoader載入,如果不是的話就會丟擲一個SecurityException異常。只有啟動類載入器載入的類才能夠呼叫 Unsafe 類中的方法,這是為了防止這些方法在不可信的程式碼中被呼叫。

Unsafe 類實現的功能可以被分為 8 類:記憶體操作、記憶體屏障、物件操作、陣列操作、CAS 操作、執行緒排程、Class 操作、系統資訊。

建立例項

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        User user = new User(2);
        fieldTest(getUnsafe(), user);
    }

    // 利用反射獲得 Unsafe 類中已經例項化完成的單例物件
    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }

    public static void fieldTest(Unsafe unsafe, User user) throws NoSuchFieldException {
        // 獲取到了物件中欄位的偏移地址,這個偏移地址不是記憶體中的絕對地址而是一個相對地址
        long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
        System.out.println("offset:" + fieldOffset);
        // 透過這個偏移地址對int型別欄位的屬性值進行讀寫操作
        unsafe.putInt(user, fieldOffset, 20);
        System.out.println("age:" + unsafe.getInt(user, fieldOffset));
        System.out.println("age:" + user.getAge());
    }

    static class User {
        private int age;

        public User() {
        }

        public User(int age) {
            this.age = age;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

    }
}

上面的例子中呼叫了 Unsafe 類的putIntgetInt方法,看一下原始碼中的方法:

// 從物件的指定偏移地址處讀取一個 int
public native int getInt(Object o, long offset);
// 從物件的指定偏移地址處寫入一個 int,即使類中的這個屬性是 private 型別的,也可以對它進行讀寫
public native void putInt(Object o, long offset, int x);

Unsafe 類中的很多基礎方法都屬於native方法,原因如下:

  • 需要用到 Java 中不具備的依賴於作業系統的特性,Java 在實現跨平臺的同時要實現對底層的控制,需要藉助其他語言發揮作用
  • 對於其他語言已經完成的一些現成功能,可以使用 Java 直接呼叫
  • 程式對時間敏感或對效能要求非常高時,有必要使用更加底層的語言,例如 C/C++甚至是彙編

juc包的很多併發工具類在實現併發機制時,都呼叫了native方法,透過 native 方法可以打破 Java 執行時的界限,能夠接觸到作業系統底層的某些功能。對於同一個native方法,不同的作業系統可能會透過不同的方式來實現,但是對於使用者來說是透明的,最終都會得到相同的結果。

Unsafe 應用


記憶體操作

Unsafe 中,提供的下列介面都可以直接進行記憶體操作:

// 分配新的本地空間
public native long allocateMemory(long bytes);
// 重新調整記憶體空間的大小
public native long reallocateMemory(long address, long bytes);
// 將記憶體設定為指定值
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);
// 清除記憶體
public native void freeMemory(long address);
public class Main {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafe();
        // 位元組長度
        int size = 4;
        // 申請 4 位元組長度的記憶體空間
        long address1 = unsafe.allocateMemory(size);
        // 重新分配為 8 個位元組長度
        long address2 = unsafe.reallocateMemory(address1, size * 2);
        System.out.println("address1: " + address1);
        System.out.println("address2: " + address2);
        try {
            // 向每個位元組寫入內容為byte型別的 1
            unsafe.setMemory(null, address1, size, (byte) 1);
            for (int i = 0; i < 2; i++) {
                // 每次複製四個位元組
                unsafe.copyMemory(null, address1, null, address2 + size * i, 4);
            }
            // 00000001000000010000000100000001B = 16843009
            System.out.println(unsafe.getInt(address1));
            // 0000000100000001000000010000000100000001000000010000000100000001B = 72340172838076673
            System.out.println(unsafe.getLong(address2));
        } finally {
            // 透過這種方式分配的記憶體屬於堆外記憶體,是無法進行垃圾回收的
            // 需要手動呼叫freeMemory方法進行釋放,否則會產生記憶體洩漏
            unsafe.freeMemory(address1);
            unsafe.freeMemory(address2);
        }
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

記憶體屏障

指令重排序可能會帶來一個不好的結果,導致 CPU 的快取記憶體和記憶體中資料的不一致,而記憶體屏障(Memory Barrier)就是透過組織屏障兩邊的指令重排序從而避免編譯器和硬體的不正確最佳化情況。

在硬體層面上,記憶體屏障是 CPU 為了防止程式碼進行重排序而提供的指令,不同的硬體平臺上實現記憶體屏障的方法可能並不相同。

在 Java8 中,引入了 3 個記憶體屏障的方法,它遮蔽了作業系統底層的差異,允許在程式碼中定義、並統一由 jvm 來生成記憶體屏障指令,來實現記憶體屏障的功能。Unsafe 中提供了下面三個記憶體屏障相關方法:

// 禁止讀操作重排序
public native void loadFence();
// 禁止寫操作重排序
public native void storeFence();
// 禁止讀、寫操作重排序
public native void fullFence();

記憶體屏障可以看做對記憶體隨機訪問操作中的一個同步點,使得此點之前的所有讀寫操作都執行後才可以開始執行此點之後的操作。

loadFence方法為例,它會禁止讀操作重排序,保證在這個屏障之前的所有讀操作都已經完成,並且將快取資料設為無效,重新從主存中進行載入。

基於讀記憶體屏障,我們也能實現相同的功能。下面定義一個執行緒方法,線上程中去修改flag標誌位,注意這裡的flag是沒有被volatile修飾的:

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafe();
        ChangeThread changeThread = new ChangeThread();
        new Thread(changeThread).start();
        while (true) {
            boolean flag = changeThread.flag;
            // 加入讀記憶體屏障,使主執行緒中的快取資料設為無效,必須重新從主存中進行載入。
            // 如果沒有記憶體屏障,主執行緒中的flag一直都是舊值false,無法結束迴圈
            unsafe.loadFence();
            if (flag) {
                System.out.println("detected flag changed");
                break;
            }
        }
        System.out.println("main thread end");
    }

    static class ChangeThread implements Runnable {
        // 加上 volatile 後,註釋掉 loadFence,主執行緒迴圈一樣能退出
        boolean flag = false;

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("before subThread change flag");
            flag = true;
            System.out.println("after subThread change flag");
        }
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

執行中的執行緒不是直接讀取主記憶體中變數的,只能操作自己工作記憶體中的變數,然後同步到主記憶體中,並且執行緒的工作記憶體是不能共享的。子執行緒藉助於主記憶體,將修改後的結果同步給了主執行緒,進而修改主執行緒中的工作空間,跳出迴圈。

物件操作

1.除了前面的putIntgetInt方法外,Unsafe 提供了 8 種基礎資料型別以及Objectputget方法,並且所有的put方法都可以越過訪問許可權,直接修改記憶體中的資料。

閱讀 openJDK 原始碼中的註釋可以發現,基礎資料型別和Object的讀寫稍有不同,基礎資料型別是直接操作的屬性值(value),而Object的操作則是基於引用值(reference value)。下面是Object的讀寫方法:

// 在物件的指定偏移地址獲取一個物件引用
public native Object getObject(Object o, long offset);
// 在物件指定偏移地址寫入一個物件引用
public native void putObject(Object o, long offset, Object x);

除了物件屬性的普通讀寫外,Unsafe 還提供了 volatile 讀寫有序寫入方法。volatile讀寫方法的覆蓋範圍與普通讀寫相同,包含了全部基礎資料型別和Object型別,以int型別為例:

// 在物件的指定偏移地址處讀取一個int值,支援volatile load語義
public native int getIntVolatile(Object o, long offset);
// 在物件指定偏移地址處寫入一個int,支援volatile store語義
public native void putIntVolatile(Object o, long offset, int x);

相對於普通讀寫來說,volatile讀寫具有更高的成本,因為它需要保證可見性和有序性。在執行get操作時,會強制從主存中獲取屬性值,在使用put方法設定屬性值時,會強制將值更新到主存中,從而保證這些變更對其他執行緒是可見的。

有序寫入的方法有以下三個:

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);

有序寫入的成本相對volatile較低,因為它只保證寫入時的有序性,而不保證可見性,也就是一個執行緒寫入的值不能保證其他執行緒立即可見。

為了解決這裡的差異性,需要對記憶體屏障的知識點再進一步進行補充,首先需要了解兩個指令的概念:

  • Load:將主記憶體中的資料複製到處理器的快取中
  • Store:將處理器快取的資料重新整理到主記憶體中

順序寫入與volatile寫入的差別在於,在順序寫時加入的記憶體屏障型別為StoreStore型別,而在volatile寫入時加入的記憶體屏障是StoreLoad型別。

// putOrderedXXX:Store1 -> StoreStore -> Store2
// putXXVolatile:Store1 -> StoreLoad -> Store2

在有序寫入方法中,使用的是StoreStore屏障,該屏障確保Store1立刻重新整理資料到記憶體,這一操作先於Store2以及後續的儲存指令操作。

而在volatile寫入中,使用的是StoreLoad屏障,該屏障確保Store1立刻重新整理資料到記憶體,這一操作先於Load2及後續的裝載指令,並且,StoreLoad屏障會使該屏障之前的所有記憶體訪問指令,包括儲存指令和訪問指令全部完成之後,才執行該屏障之後的記憶體訪問指令。

在上面的三類寫入方法中,在寫入效率方面,按照putputOrderputVolatile的順序效率逐漸降低。

2.使用 Unsafe 的allocateInstance方法,允許我們使用非常規的方式進行物件的例項化,首先定義一個實體類,並且在構造方法中對其成員變數進行賦值操作:

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        A a1 = new A();
        // 1
        System.out.println(a1.getB());

        A a2 = A.class.newInstance();
        // 1
        System.out.println(a2.getB());

        A a3 = (A) getUnsafe().allocateInstance(A.class);
        // 0
        // 透過allocateInstance方法建立物件過程中,不會呼叫類的構造方法
        System.out.println(a3.getB());
    }

    static class A {
        private int b;

        // 如果將 A 類的構造方法改為private型別,將無法透過構造方法和反射建立物件,但allocateInstance方法仍然有效。
        public A() {
            this.b = 1;
        }

        public int getB() {
            return b;
        }
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

使用這種方式建立物件時,只用到了Class物件,所以說如果想要跳過物件的初始化階段或者跳過構造器的安全檢查,就可以使用這種方法。

陣列操作

在 Unsafe 中,可以使用arrayBaseOffset方法獲取陣列中第一個元素的偏移地址,使用arrayIndexScale方法可以獲取陣列中元素間的偏移地址增量。

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafe();

        String[] array = new String[]{"str1str1str", "str2", "str3"};
        // 獲取陣列中第一個元素的偏移地址
        int baseOffset = unsafe.arrayBaseOffset(String[].class);
        System.out.println("第一個元素的偏移地址 baseOffset = " + baseOffset);
        int scale = unsafe.arrayIndexScale(String[].class);
        // 獲取陣列中元素間的偏移地址增量
        System.out.println("偏移地址增量 scale = " + scale);

        for (int i = 0; i < array.length; i++) {
            int offset = baseOffset + scale * i;
            System.out.println(offset + " : " + unsafe.getObject(array, offset));
        }
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

CAS 操作

在 Unsafe 類中,提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法來實現的對Objectintlong型別的 CAS 操作。

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

引數中o為需要更新的物件,offset是物件o中整形欄位的偏移量,如果這個欄位的值與expected相同,則將欄位的值設為x這個新值,並且此更新是不可被中斷的,也就是一個原子操作。

public class Main {
    private volatile int a;

    public static void main(String[] args) {
        Main obj = new Main();

        new Thread(() -> {
            for (int i = 1; i < 5; i++) {
                obj.increment(i);
                System.out.print(obj.a + " ");
            }
        }).start();
        new Thread(() -> {
            for (int i = 5; i < 10; i++) {
                obj.increment(i);
                System.out.print(obj.a + " ");
            }
        }).start();
        // 依次輸出 1 2 3 4 5 6 7 8 9
    }

    private void increment(int x) {
        Unsafe unsafe = null;
        try {
            unsafe = getUnsafe();
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
        // 在呼叫compareAndSwapInt方法後,會直接返回true或false的修改結果,因此需要我們在程式碼中手動新增自旋的邏輯。
        // 在AtomicInteger類的設計中,也是採用了將compareAndSwapInt的結果作為迴圈條件,直至修改成功才退出死迴圈的方式來實現的原子性的自增操作。
        while (true) {
            try {
                long fieldOffset = unsafe.objectFieldOffset(Main.class.getDeclaredField("a"));
                // 只有在a的值等於傳入的引數x減一時,才會將a的值變為x
                if (unsafe.compareAndSwapInt(this, fieldOffset, x - 1, x))
                    break;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

執行緒排程

Unsafe 類中提供了parkunparkmonitorEntermonitorExittryMonitorEnter方法進行執行緒排程。

LockSupport的原始碼,可以看到它也是呼叫的 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);
}

LockSupport 的park方法呼叫了 Unsafe 的park方法來阻塞當前執行緒,此方法將執行緒阻塞後就不會繼續往後執行,直到有其他執行緒呼叫unpark方法喚醒當前執行緒。

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafe();

        // 子執行緒開始執行後先進行睡眠,確保主執行緒能夠呼叫park方法阻塞自己,子執行緒在睡眠 3 秒後,呼叫unpark方法喚醒主執行緒,使主執行緒能繼續向下執行
        Thread mainThread = Thread.currentThread();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                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");
        /*
            park main mainThread
            subThread try to unpark mainThread
            unpark mainThread success
         */
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

Unsafe 原始碼中monitor相關的三個方法已經被標記為deprecated,不建議被使用:

// 獲得物件鎖
@Deprecated
public native void monitorEnter(Object var1);
// 釋放物件鎖
@Deprecated
public native void monitorExit(Object var1);
// 嘗試獲得物件鎖
@Deprecated
public native boolean tryMonitorEnter(Object var1);

monitorEnter方法用於獲得物件鎖,monitorExit用於釋放物件鎖,如果對一個沒有被monitorEnter加鎖的物件執行此方法,會丟擲IllegalMonitorStateException異常。tryMonitorEnter方法嘗試獲取物件鎖,如果成功則返回true,反之返回false

Class 操作

1.靜態屬性讀取相關的方法:

// 獲取靜態屬性的偏移量
public native long staticFieldOffset(Field f);
// 獲取靜態屬性的物件指標
public native Object staticFieldBase(Field f);
// 判斷類是否需要例項化(用於獲取類的靜態屬性前進行檢測)
public native boolean shouldBeInitialized(Class<?> c);
public class Main {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafe();

        User user = new User();
        // false,註釋掉上一行後就是true
        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);
    }

    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        // Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

class User {
    public static String name = "Spring";
    int age;
}

在上面的程式碼中,獲取Field物件需要依賴Class,而獲取靜態變數的屬性時則不再依賴於Class

在上面的程式碼中,首先建立一個User物件,這是因為如果一個類沒有被例項化,那麼它的靜態屬性也不會被初始化,最後獲取的欄位屬性將是null。所以在獲取靜態屬性前,需要呼叫shouldBeInitialized方法,判斷在獲取前是否需要初始化這個類。如果刪除建立 User 物件的語句,執行結果會變為:true null

2.使用defineClass方法允許程式在執行時動態地建立一個類,方法定義如下:

public native Class<?> defineClass(String name, byte[] b, int off, int len,
                                    ClassLoader loader,ProtectionDomain protectionDomain);

在實際使用過程中,可以只傳入位元組陣列、起始位元組的下標以及讀取的位元組長度,預設情況下,類載入器(ClassLoader)和保護域(ProtectionDomain)來源於呼叫此方法的例項。下面的例子中實現了反編譯生成後的 class 檔案的功能:

private static void defineTest() {
     String fileName="xxx\\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();
     }
 }

在上面的程式碼中,首先讀取了一個class檔案並透過檔案流將它轉化為位元組陣列,之後使用defineClass方法動態的建立了一個類,並在後續完成了它的例項化工作,且透過這種方式建立的類,會跳過 JVM 的所有安全檢查。

除了defineClass方法外,Unsafe 還提供了一個defineAnonymousClass方法:

public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

使用該方法可以動態的建立一個匿名類,Lambda 表示式中就是使用 ASM 動態生成位元組碼的,然後利用該方法定義實現相應的函式式介面的匿名類。

在 JDK 15 釋出的新特性中,在隱藏類(Hidden classes)一條中,指出將在未來的版本中棄用 Unsafe 的defineAnonymousClass方法。

系統資訊

Unsafe 中提供的addressSizepageSize方法用於獲取系統資訊,呼叫addressSize方法會返回系統指標的大小,如果在 64 位系統下預設會返回 8,而 32 位系統則會返回 4。呼叫 pageSize 方法會返回記憶體頁的大小,值為 2 的整數冪。使用下面的程式碼可以直接進行列印:

System.out.println(unsafe.addressSize());
System.out.println(unsafe.pageSize());

這兩個方法的應用場景比較少,在java.nio.Bits類中,在使用pageCount計算所需的記憶體頁的數量時,呼叫了pageSize方法獲取記憶體頁的大小。另外,在使用copySwapMemory方法複製記憶體時,呼叫了addressSize方法,檢測 32 位系統的情況。

相關文章