最近为了更加深入了解NIO的实现原理,学习NIO的源码时,遇到了一个问题。即在WindowsSelectorImpl中的

pollWrapper属性,当我点进去查看它的PollArrayWrapper类型时,发现它和AllocatedNativeObject类型有关,而AllocatedNativeObject继承了NativeObject类,随着又发现了NativeObject是基于一个Unsafe类实现的。不安全的类????

Unsafe

Unsafe,顾名思义,它真是一个不安全的类,那它为什么是不安全的呢?这就要从Unsafe类的功能说起。

学过C#的就可以知道,C#和Java的一个重要区别就是:C#可以直接操作一块内存区域,如自己申请内存和释放,而在Java中这是做不到的。而Unsafe类就可以让我们在Java中像C#一样去直接操作一块内存区域,正因为Unsafe类可以直接操作内存,意味着其速度更快,在高并发的条件之下能够很好地提高效率,所以java中很多并发框架,如Netty,都使用了Unsafe类。

虽然,Unsafe可以提高运行速度,但是因为Java本身是不支持自己直接操作内存的,这就意味着Unsafe类所做的操作不受jvm管理的,所以不会被GC(垃圾回收),需要我们手动GC,稍有不慎就会出现内存泄漏问题。且Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃。这就是为什么Unsafe被称为不安全的原因。Unsafe可以让你全力踩油门,提高自己的速度,但是它会让你的方向盘更难握稳,一不小心就可能导致车毁人亡。

源码查看

初始化

因为Unsafe的构造方法是private类型的,所以无法通过new方式实例化获取,只能通过它的getUnsafe()方法获取。又因为Unsafe是直接操作内存的,为了安全起见,Java的开发人员为Unsafe的获取设置了限制,所以想要获取它只能通过Java的反射机制来获取。

@CallerSensitive
public static Unsafe getUnsafe() {
//通过getCallerClass方法获取Unsafe类
Class var0 = Reflection.getCallerClass();
//如过该var0类不是启动类加载器(Bootstrap),则抛出异常
//正因为该判断,所以Unsafe只能通过反射获取
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
  • Reflection.getCallerClass():可以返回调用类或Reflection类,或者层层上传

  • VM.isSystemDomainLoader(ClassLoader var0):判断该类加载器是否是启动类加载器(Bootstrap)。

  • @CallerSensitive:为了防止黑客通过双重反射来提升权限,所以所有跟反射相关的接口方法都标注上CallerSensitive

所以使用下面的方式是获取不了Unsafe类的:

//使用这样的方式获取会抛出异常,因为是通过系统类加载器加载(AppClassLoader)
public class Test {
public static void main(String[] args) {
Unsafe unsafe = Unsafe.getUnsafe();
}
}

那怎么才用使用启动类加载Unsafe类并获取它呢?在Unsafe类的最下面的static代码块中有这样一段代码:

private static final Unsafe theUnsafe;
//.....
static {
registerNatives();
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
theUnsafe = new Unsafe();
//......
}

学过反射机制看过以上代码就可以知道我们可以通过getDeclaredField()返回获取Un safe类的theUnsafe属性,然后通过该属性获取Unsafe类的实例,因为在Unsafe类里的theUnsafe属性已经被new实例化了。

public class Test {
public static void main(String[] args) throws Exception {
//通过getDeclaredField方法获取Unsafe类中名为theUnsafe的属性
//注意,该属性是private类型的,所以不能用getField获取,只能用getDeclaredField
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//将该属性设为可访问
field.setAccessible(true);
//实例该属性并转为Unsafe类型
//因为theUnsafe属性是Unsafe类所在的包的启动类加载的,所以可以成功获得
Unsafe unsafe = (Unsafe)field.get(null);
}
}

获取偏移量方法

偏移量

在实际模式中,内存是被分成段的,如果想要获取内存中的某个储存单元,需知道储存单元的所在段地址(段头)和偏移量,即使你知道该储存单元的实际地址。而偏移量就是实际地址与所在段地址(段头)的距离,偏移量=实际地址-所在段地址(段头)

举个例子,假设有个书架,我需要找由左到右、由上到下数的第1024本书,那我只能一本本的数,直到数到第1024本,但如果我知道书架的第4层的第一本书是第1000本书,那我只用从第1000本书开始数,数到1024,只需数1024-1000=24本。在这里,书架是内存,要找的书就是储存单元,书架的第4层就是内存段,第4层的第一本书即书架的第1000本书就是段地址(段头),第1024本书就是实际地址,而偏移量的就是第1000本书到第1024本书的距离24.

public native long objectFieldOffset(Field var1);

获取非静态变量var1的偏移量。

public native long staticFieldOffset(Field var1);

获取静态变量var1的偏移量。

public native Object staticFieldBase(Field var1);

获取静态变量var1的实际地址,配合staticFieldOffset方法使用,可求出变量所在的段地址

public native int arrayBaseOffset(Class<?> var1);

获取数组var1中的第一个元素的偏移量,即数组的基础地址。

在内存中,数组的存储是以一定的偏移量增量连续储存的,如数组的第一个元素的实际地址为24,偏移量为4,而数组的偏移量增量为1,那数组的第二个元素的实际地址就是25,偏移量为5.

public native int arrayIndexScale(Class<?> var1);

获取数组var1的偏移量增量。结合arrayBaseOffset(Class<?> var1)方法就可以求出数组中各个元素的地址。

操作属性方法

public native Object getObject(Object var1, long var2);

获取var1对象中偏移量为var2的Object对象,该方法可以无视修饰符限制。相同方法有getInt、getLong、getBoolean等。

public native void putObject(Object var1, long var2, Object var4);

将var1对象中偏移量为var2的Object对象的值设为var4,该方法可以无视修饰符限制。相同的方法有putInt、putLong、putBoolean等。

public native Object getObjectVolatile(Object var1, long var2);

功能与getObject(Object var1, long var2)一样,但该方法可以保证读写的可见性和有序性,可以无视修饰符限制。相同的方法有getIntVolatile、getLongVolatile、getBooleanVolatile等。

public native void putObjectVolatile(Object var1, long var2, Object var4);

功能与putObject(Object var1, long var2, Object var4)一样,但该方法可以保证读写的可见性和有序性,可以无视修饰符限制。相同的方法有putIntVolatile、putLongVolatile、putBooleanVolatile等。

public native void putOrderedObject(Object var1, long var2, Object var4);

功能与putObject(Object var1, long var2, Object var4)一样,但该方法可以保证读写的有序性(不保证可见性),可以无视修饰符限制。相同的方法有putOrderedInt、putOrderedLong等。

操作内存方法

public native int addressSize();

获取本地指针大小,单位为byte,通常值为4或8。

public native int pageSize();

获取本地内存的页数,该返回值会是2的幂次方。

public native long allocateMemory(long var1);

开辟一块新的内存块,大小为var1(单位为byte),返回新开辟的内存块地址。

public native long reallocateMemory(long var1, long var3);

将内存地址为var3的内存块大小调整为var1(单位为byte),返回调整后新的内存块地址。

public native void setMemory(long var2, long var4, byte var6);

从实际地址var2开始将后面的字节都修改为var6,修改大小为var4(通常为0)。

public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

从对象var1中偏移量为var2的地址开始复制,复制到var4中偏移量为var5的地址,复制大小为var7。

当var1为空时,var2就不是偏移量而是实际地址,当var4为空时,var5就不是偏移量而是实际地址。

public native void freeMemory(long var1);

释放实际地址为var1的内存。

线程挂起和恢复方法

public native void unpark(Object var1);

将被挂起的线程var1恢复,由于其不安全性,需保证线程var1是存活的。

public native void park(boolean var1, long var2);

当var2等于0时,线程会一直挂起,知道调用unpark方法才能恢复。

当var2大于0时,如果var1为false,这时var2为增量时间,即线程在被挂起var2秒后会自动恢复,如果var1为true,这时var2为绝对时间,即线程被挂起后,得到具体的时间var2后才自动恢复。

CAS方法

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

CAS机制相关操作,对对象var1里偏移量为var2的变量进行CAS修改,var4为期待值,var5为修改值,返回修改结果。相同方法有compareAndSwapInt、compareAndSwapLong。

类加载方法

public native boolean shouldBeInitialized(Class<?> var1);

判断var1类是否被初始。

public native void ensureClassInitialized(Class<?> var1);

确保var1类已经被初始化。

public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

定义一个类,用于动态的创建类。var1为类名,var2为类的文件数据字节数组,var3为var2的输入起点,var4为输入长度,var5为加载该类的加载器,var6为保护领域。返回创建后的类。

public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);

用于动态的创建匿名内部类。var1为需创建匿名内部类的类,var2为匿名内部类的文件数据字节数组,var3为修补对象。返回创建后的匿名内部类。

public native Object allocateInstance(Class<?> var1) throws InstantiationException;

创建var1类的实例,但是不会调用var1类的构造方法,如果var1类还没有初始化,则进行初始化。返回创建实例对象。

内存屏障方法

public native void loadFence();

所有读操作必须在loadFence方法执行前执行完毕。

public native void storeFence();

所有写操作必须在storeFence方法执行前执行完毕。

public native void fullFence();

所有读写操作必须在fullFence方法执行前执行完毕。

疑惑

看到这里可能有人会有一个疑惑,为什么这些方法都没有具体的功能实现代码呢?

在文章开头时就说过,Java不支持直接操作内存,那怎么可能用Java来具体实现功能呢。你可以发现Unsafe类内的大多方法都有native修饰符,native接口可以让你调用本地的代码文件(包括其他语言,如c语言),既然Java实现不了,那就让能实现的人来做,所以Unsafe的底层实现语言其实是C语言,这也是为什么Unsafe类内会有偏移量和指针这些Java中没有的概念了。

(以上为本人自己对Unsafe类的理解,如果有错误,欢迎各位前辈指出)

一个名为不安全的类Unsafe的更多相关文章

  1. C++创建一个名为Ellipse的椭圆类--练习

    题目描述: /*设计名为Ellipse的椭圆类*/ /* 其属性为外接矩形的左上角与右下角两个点的坐标,并能计算出椭圆的面积,并测试该类. */ 代码如下: #include<iostream& ...

  2. 16.按要求编写Java应用程序。 编写一个名为Test的主类,类中只有一个主方法; 在主方法中定义一个大小为50的一维整型数组,数组名为x,数组中存放着{1, 3,5,…,99}输出这个数组中的所有元素,每输出十个换一行;在主方法中定义一 个大小为10*10的二维字符型数组,数组名为y,正反对角线上存的是‘*’,其余 位置存的是‘#’;输出这个数组中的所有元素。

    //分类 package com.bao; public class Shuchu { int[]yi=new int[50]; String[][]er=new String[10][10]; vo ...

  3. 按要求编写Java应用程序。 编写一个名为Test的主类,类中只有一个主方法; 在主方法中定义一个大小为50的一维整型数组,数组名为x,数组中存放着{1, 3,5,…,99}输出这个数组中的所有元素,每输出十个换一行;在主方法中定义一 个大小为10*10的二维字符型数组,数组名为y,正反对角线上存的是‘*’,其余 位置存的是‘#’;输出这个数组中的所有元素。

    int[]x=new int [50]; char[][]y=new char[10][10]; int j=1,w=0; for(int i=0;i<50;i++) { x[i]=j; j+= ...

  4. 编写一个名为Test的主类,类中只有一个主方法; 在主方法中定义一个大小为50的一维整型数组,数组名为x,数组中存放着{1, 3,5,…,99}输出这个数组中的所有元素,每输出十个换一行;在主方法中定义一 个大小为10*10的二维字符型数组,数组名为y,正反对角线上存的是‘*’,其余 位置存的是‘#’;输出这个数组中的所有元素。

    package liu0915; import java.util.Random; public class Test0915sz { public static void main(String[] ...

  5. 3.实现一个名为Person的类和它的子类Employee,Employee有两个子类Faculty 和Staff。

    23.实现一个名为Person的类和它的子类Employee,Employee有两个子类Faculty 和Staff. 具体要求如下: (1)Person类中的属性有:姓名name(String类型) ...

  6. 实现一个名为Person的类和它的子类Employee,Manager是Employee的子类,设计一个方法add用于涨工资,普通员工一次能涨10%,经理能涨20%。

    1.实现一个名为Person的类和它的子类Employee,Manager是Employee的子类,设计一个方法add用于涨工资,普通员工一次能涨10%,经理能涨20%.具体要求如下:(1)Perso ...

  7. C# 随机给一个全部信息都未知的类类型,如何获取该类的类名、属性个数、属性名、属性的数据类型、属性值?

    一.场景假设 假设现在有一个泛型类T的实例对象t,该T类的全部信息都未知. 要求:打印输出实例对象t的类名.属性个数.属性名.属性的数据类型.属性值. 二.解决问题 1.我们根据输出的内容要求定义一个 ...

  8. Java中的魔法类-Unsafe

    Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别.不安全操作的方法,如直接访问系统内存资源.自主管理内存资源等,这些方法在提升Java运行效率.增强Java语言底层资源操作能 ...

  9. WebForms UnobtrusiveValidationMode 需要“jquery”ScriptResourceMapping。请添加一个名为 jquery (区分大小写)的 ScriptResourceMapping。

    WebForms UnobtrusiveValidationMode 需要“jquery”ScriptResourceMapping.请添加一个名为 jquery (区分大小写)的 ScriptRes ...

随机推荐

  1. Python3基础——函数

    ython 函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.你已经知道Python提供了许多内建函数,比如print().但你也可 ...

  2. 深入研究RocketMQ消费者是如何获取消息的

    前言 小伙伴们,国庆都过的开心吗?国庆后的第一个工作日是不是很多小伙伴还沉浸在假期的心情中,没有工作状态呢? 那王子今天和大家聊一聊RocketMQ的消费者是如何获取消息的,通过学习知识来找回状态吧. ...

  3. 部署MongoDB-4.2.7

    二进制部署很简单 创建存放软件目录下载解压 存放数据和日志及配置文件路径需要手工进行创建 mkdir -p /application/tools/ cd /application/tools/ wge ...

  4. ubuntu19.10 系统需要安装的软件

    将ubuntu18 升级到ubuntu19 期间好几次卡在启动界面,比较担心要不要重装系统,有幸后来正常了.明显感觉操作快了不少.下半年稳定版就出来,到时候免不了再折腾一番,提前把安全记录做好. 下面 ...

  5. MeteoInfoLab脚本示例:合并数组

    对于全球数据来说,经度要么是-180 - 180,要么是0 - 360,都会存在边界数据不连续的问题.比如0 - 360的数据,怎么得到 -20 - 30度的连续格点数据就是个问题(跨越了数据的经度边 ...

  6. kubernetes:用label让pod在指定的node上运行(kubernetes1.18.3)

    一,为什么要为node指定label? 通常scheduler会把pod调度到所有可用的Node,有的情况下我们希望能把 Pod 部署到指定的 Node, 例如: 有的Node上配备了速度更快的SSD ...

  7. 每天一个linux命令:ps命令

      Linux中的ps命令是Process Status的缩写.ps命令用来列出系统中当前运行的那些进程.ps命令列出的是当前那些进程的快照,就是执行ps命令的那个时刻的那些进程,如果想要动态的显示进 ...

  8. faker切换user-agent

    import random import requests url = "http://tool.yeves.cn" import faker fake = faker.Faker ...

  9. Groovy中如何向已有的类添加新方法

    Groovy 中有多种途径实现向原有类添加方法,具体有如下几种: MOP(meta object protocol) -- 详见 ExpandoMetaClass 扩展方法 -- GDK采用的此方法 ...

  10. APP脱壳方法三

    第一步 手机启动frida服务 第二步 手机打开要脱壳的app 第三步编辑hook代码 agent.js /* * Author: hluwa <hluwa888@gmail.com> * ...