“This is the 24th day of my participation in the First Challenge 2022. For details: First Challenge 2022.”
The Unsafe class in Java is the foundation for implementing synchronized components in JUC.
1 overview
This article is based on JDK1.8.
The Unsafe class, located in the rt.jar package, provides hardware-level atomic operations. Its methods are native methods that use JNI to access the native C++ implementation library. This provides some lower-level functionality to bypass the JVM and improve program efficiency.
JNI: Java Native Interface. Enables Java to interact directly with other native types of languages, such as C and C++.
Unsafe is intended to extend the expressive power of the Java language and enable the implementation of core library functions in higher-level (Java layer) code that would otherwise be implemented in lower-level (C layer) code. These include direct memory allocation/release/access, atomic/volatile support for low-level hardware, creating uninitialized objects, manipulating object fields and methods with offsets, and thread lockless suspension and recovery.
The “layout” of a Java object is where the various parts of a Java object are placed in memory, including instance fields and some metadata. The broadening method abstracts the layout of objects by providing the objectFieldOffset() method to retrieve the offset of a field from the “start address” of a Java object. Methods such as getInt, getLong, and getObject are also provided to access a field of a Java object using the offset obtained earlier.
Unsafe can be roughly summed up as:
- Memory management, including allocating and releasing memory.
- Unconventional object instantiation.
- Manipulate classes, objects, variables.
- Custom oversized array operations.
- Multithreaded synchronization. This operation includes locking mechanism and CAS operation.
- Thread suspends and resumes.
- Memory barriers.
2 the API,
The Unsafe list includes 82 public native methods, as well as dozens of other methods based on the 82, making a total of 114 methods.
2.1 Initialization method
Broadening is the singleton schema class, as seen directly in the source code:
private static final Unsafe theUnsafe;
// Constructor is private
private Unsafe(a) {}// Static block initialization
static {
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
theUnsafe = new Unsafe();
}
// Static method to get the instance
@CallerSensitive
public static Unsafe getUnsafe(a) {
Class var0 = Reflection.getCallerClass();
if(! VM.isSystemDomainLoader(var0.getClassLoader())) {throw new SecurityException("Unsafe");
} else {
returntheUnsafe; }}Copy the code
From the above code, it seems possible to get the instance through the getUnsafe() method, but if we call this method we get an exception:
java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
/ /..........................................
Copy the code
We can actually see that the getUnsafe() method has a ** @callersensitive ** annotation. Because of this annotation, we need to make a permission judgment when executing: Only a class loaded by the BootStrap classLoader can call a method in this class (for example, a class in rt.jar can call this method, because the class name indicates that it is “unsafe” and can not be called at will, which will be discussed later). Obviously our class is loaded by the AppClassLoader, so we throw an exception here.
Therefore, the simplest way to use Unsafe is to get the Unsafe instance based on reflection, as follows:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Copy the code
2.2 Class, object, and variable related methods
This includes getting or setting the value of a variable based on the offset address, getting or setting the value of an array element based on the offset address, class initialization, and unconventional object creation.
2.2.1 Object Operations
/* Object operation */
/* Get the value of the object field */
// Get the reference value from the given Java variable. This is actually getting the value of a property in a Java object O that has an offset address. This method can override the suppression of modifiers by ignoring the private, protected, and default modifiers.
// Similar methods are getInt, getDouble, etc.
public native Object getObject(Object o, long offset);
// This method is similar to the getObject function above, but with the added semantics of 'volatile' loading, which forces the property values from main memory. Similar methods are getIntVolatile, getDoubleVolatile, and so on.
// This method requires that the attributes being used be volatile, otherwise it has the same function as getObject.
public native Object getObjectVolatile(Object o, long offset);
/* Change the value of the object field */
// Set the value of the property of the Java object O at offset to x. This method can override the suppression of modifiers, i.e. ignoring the private, protected, and default modifiers. Used to modify the value of a non-basic data type.
// putInt, putDouble, etc., are similar methods for modifying values of basic data types.
public native void putObject(Object o, long offset, Object x);
// This method is similar to the putObject function above, but with the addition of the 'volatile' loading semantics, which means that setting values forces (the JMM ensures that all state updates of objects between acquiring and releasing the lock are made after the lock is released) to main memory so that these changes are visible to other threads.
// Similar methods are putIntVolatile, putDoubleVolatile, etc. This method requires that the attributes being used be volatile, otherwise it has the same functionality as putObject.
public native void putObjectVolatile(Object o, long offset, Object x);
// Set the Object field corresponding to the offset address in the o Object to the specified value x. This is an ordered or delayed putObjectVolatile method, and there is no guarantee that the value change will be immediately visible to other threads.
This is valid only if the field is volatile and expected to be modified. Similar methods are putOrderedInt and putOrderedLong.
// It will eventually be set to x, but other threads may still be able to read the old value for a short time later. For more information on this approach, see the article "How does AtomicLong. LazySet Work?" translated by Concurrent Programming. The address of the article is http://ifeve.com/how-does-atomiclong-lazyset-work/.
public native void putOrderedObject(Object o, long offset, Object x);
/* Gets the offset of the object's field relative to the object's address */
// Returns the location (offset address) of the given static property in its class's storage allocation. That is, the offset relative to classname. class by which fields can be quickly located.
// Note: this method is for static attributes only. Exceptions are thrown for non-static attributes.
public native long staticFieldOffset(Field f);
// Returns the location (offset address) of the given non-static property in its class's storage allocation. That is, the offset from the field to the object header, by which you can quickly locate the field.
// Note: this method only applies to non-static attributes. Using static attributes throws exceptions.
public native long objectFieldOffset(Field f);
// Returns the location of the given static property, used with the staticFieldOffset method. In effect, the return value of this method is a memory snapshot of the Class object where the static property resides
The Object returned by this method may be null. It is just a 'cookie' and not a real Object. Do not use the methods used to get and set properties of its instance directly. All it does is make it easy to call any of the aforementioned methods like getInt(Object, Long) and so on.
public native Object staticFieldBase(Field f);
/* Create an object */
// Bypass the constructor and initialize the code to create objects unconventionally
public native Object allocateInstance(Class
cls) throws InstantiationException;
Copy the code
2.2.2 class related
// Check whether the given class needs to be initialized. This is usually used when retrieving a class's static properties (because if a class is not initialized, its static properties are not initialized).
// This method returns false if and only if the ensureClassInitialized method is disabled.
public native boolean shouldBeInitialized(Class
c);
// Check whether the given class is already initialized. This is usually used when retrieving a class's static properties (because if a class is not initialized, its static properties are not initialized).
public native void ensureClassInitialized(Class
c);
// Define a class and return an instance of the class. This method skips all security checks for the JVM. By default, instances of ClassLoader and ProtectionDomain should come from the caller.
public nativeClass<? > defineClass(String name,byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
/// Define an anonymous class associated with Java8 lambda expressions that will be used to implement the corresponding functional interface. See the closing article link.
public nativeClass<? > defineAnonymousClass(Class<? > hostClass,byte[] data, Object[] cpPatches);
Copy the code
2.2.3 Array Element correlation
// Returns the offset address (base offset address) of the first element of the array type. If the arrayIndexScale method does not return a scale factor of 0, you can access all elements of the array by combining the underlying offset address with the scale factor.
// Unsafe already initializes many similar constants such as ARRAY_BOOLEAN_BASE_OFFSET.
public native int arrayBaseOffset(Class
arrayClass);
// Returns the size of a single element in the array. The addresses of the elements in the array are sequential.
// Unsafe already initializes many similar constants such as ARRAY_BOOLEAN_INDEX_SCALE.
public native int arrayIndexScale(Class
arrayClass);
Copy the code
2.3 Memory Management
This section includes allocateMemory (allocateMemory), reallocateMemory (realallocate memory), copyMemory (copyMemory), and freeMemory (freeMemory) ), getAddress, addressSize, pageSize, getInt, getIntVolatile, getIntVolatile It also supports volatile semantics), putInt (writing integers to specified memory addresses), putIntVolatile (writing integers to specified memory addresses with volatile semantics), and putOrderedInt (writing integers to specified memory addresses, ordered or delayed methods). GetXXX and putXXX contain various basic types of operations.
Using copyMemory, we can implement a generic copy of an object without having to clone every object. Of course, this generic method can only do a shallow copy of an object.
Unsafe allocates memory that is not restricted by integer. MAX_VALUE and is allocated in non-heap memory. Caution should be exercised when using Unsafe memory: Forgetting to manually reclaim will result in memory leaks and can be done manually using the Unsafe#freeMemory method. An invalid address access can cause the JVM to crash. It can be used when large contiguity areas need to be allocated, real-time programming (with no tolerance for JVM latency), because direct memory is more efficient. For details, see the NIO source code for Java, which uses this technique.
The Unsafe allocateMemory and setMemory methods are used in the DirectByteBuffer constructor to allocate immediate memory using the ByteBuffer#allocateDirect method: Unsafe.allocatememory allocates memory, unsafe.setMemory initializes memory, and then builds a virtual reference Cleaner object to track the garbage collection of DirectByteBuffer objects. When DirectByteBuffer is garbage collected, the allocated out-of-heap memory is released together (by calling the Unsafe#freeMemory method in the Cleaner).
// Gets the size (in bytes) of the local pointer, usually 4(32-bit systems) or 8(64-bit systems). The constant ADDRESS_SIZE is called to this method.
public native int addressSize(a);
// Gets the number of pages in local memory, a power of 2.
// A static method in Bits, a tool class under java.nio, that calculates the number of memory pages required to apply for memory. It relies on the pageSize method in Unsafe to obtain the size of system memory pages for subsequent calculation logic
public native int pageSize(a);
// Allocate a new block of local memory. Use bytes to specify the size of the block, in bytes, and return the address of the newly allocated memory. Memory blocks can be freed using the freeMemory method, or resized using the reallocateMemory method.
If bytes is negative or more than that, IllegalArgumentException is thrown. OutOfMemoryError is thrown if the system refuses to allocate memory.
public native long allocateMemory(long bytes);
// Resize the local memory block using the specified memory address address. The adjusted memory block size is specified in bytes. Memory blocks can be freed using the freeMemory method, or resized using the reallocateMemory method.
If bytes is negative or more than that, IllegalArgumentException is thrown. OutOfMemoryError is thrown if the system refuses to allocate memory.
public native long reallocateMemory(long address, long bytes);
// Sets the value in the given memory block. The address of the memory block is determined by both the object reference O and the offset address. If the object reference O is null, offset is the absolute address. The third parameter is the size of the memory block. If allocateMemory is used to create memory, the value should be the same as the parameter of allocateMemory. Value is a fixed value set to 0(see Netty's DirectByteBuffer).
Public native void setMemory(long offset, long bytes, byte value); public native void setMemory(long offset, long bytes, byte value); , equivalent to setMemory(null, long offset, long Bytes, byte value); .
public native void setMemory(Object o, long offset, long bytes, byte value);
// Free memory
public native void freeMemory(long address);
Copy the code
2.4 Multi-Thread Synchronization
This includes monitor locking, unlocking, and CAS-related methods. This section includes monitorEnter, tryMonitorEnter, monitorExit, compareAndSwapInt, compareAndSwap, etc. MonitorEnter, tryMonitorEnter, and monitorExit have been marked as deprecated and are not recommended.
The Unsafe class’s CAS operation is probably the most used, providing a new alternative to Java’s locking mechanism, as classes such as AtomicInteger implement it. This is an optimistic lock that is generally assumed to have no race conditions in most cases, and if the operation fails, it will be retried until it succeeds.
// Lock the object, which must be unlocked through monitorExit. This method is experimentally reentrant, meaning it can be called multiple times and then unlocked by calling monitorExit multiple times.
@Deprecated
public native void monitorEnter(Object o);
/ / unlock object, the premise is that objects must have call monitorEnter lock, otherwise throw IllegalMonitorStateException anomalies.
@Deprecated
public native void monitorExit(Object o);
// Try to lock the object, return true if the lock succeeded, false otherwise. You must use the monitorExit method to unlock.
@Deprecated
public native boolean tryMonitorEnter(Object o);
// Perform CAS operations on Object objects. That is, the corresponding Java variable reference O atomically updates the value of the property offset from O to x. Return true if and only if the current value of the property offset from O is expected, otherwise return false.
//o: target Java variable reference. Offset: The offset address of the target attribute in the target Java variable. Expected: The expected current value of the target property in the target Java variable. X: The target updated value of the target property in the target Java variable.
// Similar methods are compareAndSwapInt and compareAndSwapLong, GetAndAddInt getAndAddLong getAndSetInt getAndSetLong getAndSetObject getAndAddInt getAndAddLong getAndSetObject getAndAddLong getAndSetObject
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
// Get the current value of volatile semantics offset from obj and set the value of volatile semantics to update
long getAndSetLong(Object obj, long offset, long update)
// Get the current value of volatile semantics for obj with offset, and set the value to the original value +addValue
long getAndAddLong(Object obj, long offset, long addValue)
Copy the code
2.5 Thread suspension and recovery
This section includes methods such as Park and unpark.
Suspending a thread is done with the park method, which blocks the thread until a timeout or interrupt occurs. Unpark can terminate a suspended thread and restore it to normal. The suspension of threads in the entire concurrency framework is encapsulated in the LockSupport class, which has various versions of the pack methods, but ultimately calls the unsafe.park () method.
The new Java8 lock StampedLock uses this family of methods.
It can also be used to terminate a block caused by a previous call to park, i.e. the order in which the two methods are called can be unpark and then park.
public native void unpark(Object thread);
// Block the current thread until an unpark method appears (called), a method for unpark has appeared (called before this park method was called), the thread is interrupted, or time expires (i.e., blocking timeout).
// In the case of non-zero time, if isAbsolute is true, time is milliseconds relative to the epoch, otherwise time means nanoseconds.
public native void park(boolean isAbsolute, long time);
Copy the code
2.6 Memory Barrier
This section includes methods such as loadFence, storeFence, and fullFence. This was introduced in Java 8 to define memory barriers to avoid code reordering. If you know the memory implications of volatile and locking for the JVM, the words “memory barrier” should not be too difficult to understand, but wrapped in Java code.
LoadFence () indicates that all loads prior to this method complete before the memory barrier. Similarly, storeFence() indicates that all store operations prior to this method are completed before the memory barrier. FullFence () indicates that all load and store operations prior to this method complete before the memory barrier.
// All reads before this method must be executed before the load barrier.
public native void loadFence(a);
// All writes before this method must be executed before the Store barrier
public native void storeFence(a);
// All read and write operations before this method must be performed before the full barrier, which is a combination of the load barrier and store barrier.
public native void fullFence(a);
Copy the code
2.7 other
Nelems determines the number of samples. Nelems can only be 1 to 3, representing the average load of the system in the last 1, 5, and 15 minutes, respectively.
// This method returns -1 if the load of the system cannot be retrieved, otherwise returns the number of samples retrieved (the number of valid elements in loadavg). This method kept returning -1 in the experiment, and could have been replaced with a related method in JMX.
public native int getLoadAverage(double[] loadavg, int nelems);
// Bypass the detection mechanism and throw an exception directly. It allows us to do something special.
public native void throwException(Throwable ee);
Copy the code
3 application
3.0 Modify attribute values based on offset (pointer)
public class TestUnSafe {
static final Unsafe UNSAFE;
// The field to update
private volatile long state;
// Record the offset of the field
private static final long stateOffset;
/** * The static block initializes the unsafe field and gets the offset of the state field */
static {
try {
// Reflection gets unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
UNSAFE = (Unsafe) f.get(null);
// Get the offset
stateOffset = UNSAFE.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (Exception ex) {
throw newError(ex); }}public TestUnSafe(long state) {
this.state = state;
}
public static void main(String[] args) {
TestUnSafe testUnSafe = new TestUnSafe(0);
// Try to change the value more
boolean b = UNSAFE.compareAndSwapLong(testUnSafe, stateOffset, testUnSafe.state, 2); System.out.println(b); System.out.println(testUnSafe.state); }}Copy the code
3.1 Unconventional instantiation of objects
The most common methods of object creation, including direct new creation and reflection creation, essentially call the corresponding constructor, and with the parameter constructor, the corresponding number of arguments must be passed to complete the object instantiation.
Unsafe, on the other hand, provides the allocateInstance method, which can be created from a Class object alone without calling its constructor, initialization code, JVM security checks, and so on. And it inhibits modifier detection, meaning that the constructor can be instantiated using this method even if the constructor is private, just by lifting the class object to create the corresponding object.
Because of this feature, allocateInstance has applications in java.lang.invoke, Objenesis (which provides a way to generate objects that bypass the class constructor), and Gson (used in deserialization). During Gson deserialization, if the class has a default constructor, the instance is created by calling the default constructor through reflection, otherwise the object instance is constructed using UnsafeAllocator, UnsafeAllocator calls UnsafeAllocator’s allocateInstance to instantiate the object, ensuring that deserialization is not disruptive if the target class does not have a default constructor.
Case study:
public class UnsafeTest {
private static Unsafe UNSAFE;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
} catch (Exception ignored) {
}
}
public static void main(String[] args) {
//reflect();
unsafe();
}
/** * reflection test, comment out no argument constructor, method error; Comment open, method executed successfully,type field has value. * /
public static void reflect(a) {
/* If there is no no-argument constructor, the reflection will throw an exception using the new keyword */ inside
try{ Class<? > aClass = Class.forName("com.thread.test.juc.unsafe.User"); Constructor<? > constructor = aClass.getDeclaredConstructor(); constructor.setAccessible(true);
User o = (User) constructor.newInstance(null);
System.out.println(o);
/* The value is VIP, normal */
System.out.println(o.type);
System.out.println(o.age);
} catch(Exception e) { e.printStackTrace(); }}/** * UNSAFE test, annotation without argument constructor, still successfully constructs object, but type field is null. This is one consequence of not having the constructor go: the field is not initialized */
public static void unsafe(a) {
try {
/* Create object */ without the corresponding constructor
User user = (User) UNSAFE.allocateInstance(User.class);
user.setName("user1");
System.out.println("instance: " + user);
user.test();
/* Sets the attribute value */ using unsafe
Field name = user.getClass().getDeclaredField("name");
UNSAFE.putObject(user, UNSAFE.objectFieldOffset(name), "user2");
user.test();
/* The value is null, indicating that unsafe does not initialize fields. * /
System.out.println(user.type);
System.out.println(user.age);
} catch(Exception e) { e.printStackTrace(); }}}class User {
public String type = "VIP";
public int age = 20;
private String name;
public void setName(String name) {
this.name = name;
}
public void test(a) {
System.err.println("hello,world " + name);
}
/*private User() { System.out.println("constructor"); } * /
private User(String name) {
this.name = name; }}Copy the code
Note: In the UNSAFE test, the VIP field does not get a value. Javap -v xx.class = javap -v xx.class
- The first instruction means that an area of memory is allocated according to type
- The second instruction pushes the memory address returned by the first instruction to the top of the operand stack
- The third instruction calls the constructor of the class to perform display initialization on the field.
The safe.allocateInstance() method only does the first and second steps, allocating memory and returning the address, without the third step using the constructor. Therefore, the Unsafe. AllocateInstance () method creates objects that have only initial values, no default values and no constructor values, because it doesn’t use the new mechanism at all, manipulating memory directly to create objects.
3.2 Extremely long Array operations
The arrayBaseOffset is used in conjunction with an arrayIndexScale to locate each element in the array in memory. PutByte and getByte can obtain byte data at a specified location.
The normal Java maximum for an array is integer. MAX_VALUE, but using the Unsafe class’s memory allocation method allows for very large arrays. In fact, such data can be considered a C array, so care needs to be taken to free memory at the right time.
Create a contiguous segment of memory (an array) that is twice the maximum size allowed by Java (potentially causing a JVM crash) :
class SuperArray {
private final static int BYTE = 1;
private long size;
private long address;
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
}
}
public SuperArray(long size) {
this.size = size;
// Get the start address of the allocation
address = unsafe.allocateMemory(size * BYTE);
}
public void set(long i, byte value) {
/ / set the value
unsafe.putByte(address + i * BYTE, value);
}
public int get(long idx) {
/ / get the value
return unsafe.getByte(address + idx * BYTE);
}
public long size(a) {
return size;
}
public static void main(String[] args) {
// Double the length of integer. MAX_VALUE
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); / / 4294967294
int sum = 0;
for (int i = 0; i < 100; i++) {
array.set((long) Integer.MAX_VALUE + i, (byte) 3);
sum += array.get((long) Integer.MAX_VALUE + i);
}
System.out.println("Sum of 100 elements:" + sum); / / 300}}Copy the code
3.3 The abnormality detected by packaging is a run-time anomaly
unsafe.throwException(new IOException());
Copy the code
3.4 Dynamically creating classes at runtime
The standard method for dynamically loading classes is class.forname ()(memorably from writing JDBC programs). Using Unsafe, Java Class files can also be dynamically loaded. This is done by reading a.class file into a byte data group and passing it to the defineClass method.
public class CreateClass {
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch(Exception e) { e.printStackTrace(); }}//Method to read .class file
private static byte[] getClassContent() throws Exception {
File f = new File("target/classes/com/thread/test/juc/unsafe/A.class");
FileInputStream input = new FileInputStream(f);
byte[] content = new byte[(int) f.length()];
input.read(content);
input.close();
return content;
}
public static void main(String[] args) throws Exception {
//Sample code to creat classes
byte[] classContents = getClassContent();
Class c = unsafe.defineClass(null, classContents, 0, classContents.length, CreateClass.class.getClassLoader(), null);
c.getMethod("a").invoke(c.newInstance()); //aaaa}}class A {
public void a(a) {
System.out.println("aaaa"); }}Copy the code
3.5 Implementing Shallow Cloning
The shallow clone is realized by obtaining the memory directly. Copy the bytecode of an object to another location in memory, and then convert the object to the cloned object type. For convenience, S represents the object to be cloned, D represents the cloned object, SD represents the memory address of S, DD represents the memory address of D, and SIZE represents the SIZE of the object in memory.
- Gets the memory address SD of the original object.
- Calculate the SIZE of the original object in memory.
- The SIZE of the newly allocated memory is the SIZE of the original object. Record the address DD of the newly allocated memory.
- Copy the memory SIZE from the original object memory address SD to DD.
- The SIZE of memory at DD is a shallow clone of the original object, which is cast to the source object type.
4 Summary and attention
From the introduction above, we can see how powerful and interesting Unsafe is, but it is not officially recommended to use the Unsafe class directly in code. Even from its name, “Unsafe” — that must mean Unsafe. So what’s not safe? We know that C or C++ can manipulate Pointers directly, and pointer manipulation is very unsafe, which is why Java “gets rid of” Pointers.
Going back to the Unsafe class, the Unsafe class contains a large number of methods for manipulating pointer offsets. These offsets must be computed on their own, and if improperly used, can cause a lot of uncontrolled disaster for the application, causing the JVM to crash. Unsafe classes should therefore be used with extreme caution, especially for production-level code.
Additionally, the Unsafe class also has many ways to manipulate memory autonomously, which is direct memory that is not managed by the JVM (not GC) and needs to be managed manually, potentially becoming the source of memory leaks.
Even though Unsafe, its applications are broad. Unsafe is widely used in JUC(java.util.Concurrent) packages (mainly CAS), in netty for the convenience of direct memory, and in some highly concurrent transaction systems for the sake of CAS efficiency. Hadoop, Kafka, Akka.
In short, the Unsafe class is a double-edged sword. Maybe the “unsafe” here is just a warning for those of us who are “novices”! : : > _ < : :
5 Learning about Unsafe materials
The following is a very good article that I have referred to:
- Unsafe Application parsing
- Alibaba technical team: the underlying implementation of Lambda
- The unsafe-looking double-edged sword in JAVA
- Unsafe source code for JDK8