最近为了更加深入了解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. CentOS openssh升级到openssh-7.2版本

    查看现在的版本SSH -V 一.准备 备份ssh目录(重要) cp -rf /etc/ssh /etc/ssh.bak [ 可以现场处理的,不用设置 安装telnet,避免ssh升级出现问题,导致无法 ...

  2. ISP-OB, pedestal 以及ISP概述

    网上的直接参考资料 1. https://zhuanlan.zhihu.com/p/36896537 2. https://blog.csdn.net/m0_38049850/article/deta ...

  3. C# 软件开机启动

    如果需要查看更多文章,请微信搜索公众号 csharp编程大全,需要进C#交流群群请加微信z438679770,备注进群, 我邀请你进群! ! !---------------------------- ...

  4. rxjs入门4之rxjs模式设计

    观察者模式 (Observer Pattern) 观察者模式其实在日常编码中经常遇到,比如DOM的事件监听,代码如下 function clickHandler(event) { console.lo ...

  5. Java-JDK动态代理(AOP)使用及实现原理分析

    Java-JDK动态代理(AOP)使用及实现原理分析 第一章:代理的介绍 介绍:我们需要掌握的程度 动态代理(理解) 基于反射机制 掌握的程度: 1.什么是动态代理? 2.动态代理能够做什么? 后面我 ...

  6. 【手摸手,带你搭建前后端分离商城系统】01 搭建基本代码框架、生成一个基本API

    [手摸手,带你搭建前后端分离商城系统]01 搭建基本代码框架.生成一个基本API 通过本教程的学习,将带你从零搭建一个商城系统. 当然,这个商城涵盖了很多流行的知识点和技术核心 我可以学习到什么? S ...

  7. Ztree树节点应用

    树节点增删改查: 前台jsp页面: <% String root=request.getContextPath();//获取项目目录 %> <SCRIPT type="te ...

  8. 多测师讲解selenium _a标签定位()_高级讲师肖sir

    shift+ctrl+c 快捷键  调出元素

  9. 如何设计一个牛逼的API接口

    在日常开发中,总会接触到各种接口.前后端数据传输接口,第三方业务平台接口.一个平台的前后端数据传输接口一般都会在内网环境下通信,而且会使用安全框架,所以安全性可以得到很好的保护.这篇文章重点讨论一下提 ...

  10. nullptr解决了什么问题

    从0到NULL 在C++的世界中字面值0用来表示空指针,所以0可以当作所有指针类型的字面值.为了让语义更明确引入了NULL宏定义: #undef NULL #ifdef __cplusplus #de ...