临近春节,最近有点时间,准备顺着上篇专栏的思路写下去,建议先阅读:

juejin.im/post/684490…

武汉那几个吃野味的傻[],请藏好你们的妈

正文开始

在运行Java程序时,java虚拟机需要使用内存来存放各式各样的数据。java虚拟机规范把这些内存区域叫做运行时数据区:

而堆外内存,是指分配在java堆外的内存区域,其不受jvm管理,不会影响gc。

本文将以java.nio.DirectByteBuffer为例,来剖析堆外内存。

// Primary constructor

//

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;

}

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

att = null;

}

复制代码

预定内存

从DirectByteBuffer的构造方法中可以看出,堆外内存的分配的开始在

Bits.reserveMemory(size, cap);中。

进入Bits类,先看几个和堆外内存相关的成员属性:

private static volatile long maxMemory = VM.maxDirectMemory();

private static final AtomicLong reservedMemory = new AtomicLong();

private static final AtomicLong totalCapacity = new AtomicLong();

private static final AtomicLong count = new AtomicLong();

private static volatile boolean memoryLimitSet = false;

复制代码

maxMemory

用户设置的堆外内存最大分配量,由jvm参数-XX:MaxDirectMemorySize=配置。

reservedMemory

已使用堆外内存的大小。使用AtomicLong来保证多线程下的安全性。

totalCapacity

总容量。同样使用AtomicLong。

count

记录分配堆外内存的总份数。

memoryLimitSet

一个标记变量,有volatile关键字。用来记录maxMemory字段是否已初始化。

在分配堆外内存前,jdk使用tryReserveMemory方法实现了一个乐观锁,来保证实际分配的堆外内存总数不会大于设计的上限。

private static boolean tryReserveMemory(long size, int cap) {

// -XX:MaxDirectMemorySize limits the total capacity rather than the

// actual memory usage, which will differ when buffers are page

// aligned.

long totalCap;

while (cap <= maxMemory - (totalCap = totalCapacity.get())) {

if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {

reservedMemory.addAndGet(size);

count.incrementAndGet();

return true;

}

}

return false;

}

复制代码

在tryReserveMemory中的逻辑也比较简单,使用while循环+CAS来保证有足够的剩余空间,并更新总空间,剩余空间,和堆外内存数。

可以看出,如果CAS失败,但还有足够的容量,while循环会进入下一轮CAS更新尝试,直到更新成功或容量不足。

下面的代码段中,注释中写的很清楚:将pending状态下的引用入队并重试,如果引用中包含对应的Cleaner的话,会帮助释放堆外内存。

final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

// retry while helping enqueue pending Reference objects

// which includes executing pending Cleaner(s) which includes

// Cleaner(s) that free direct buffer memory

while (jlra.tryHandlePendingReference()) {

if (tryReserveMemory(size, cap)) {

return;

}

}

复制代码

在tryHandlePendingReference方法中,代码只有4行:

SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {

public boolean tryHandlePendingReference() {

return Reference.tryHandlePending(false);

}

});

复制代码

相信看过上一篇讲解虚引用的专栏的读者到这里已经明白这里是怎样做的堆外内存释放了:

jlra.tryHandlePendingReference()实际上调用方法与jdk中处理pending状态引用Reference-handler线程调用的是同一个方法。

关于Reference-handler线程,详见:juejin.im/post/684490…

随后,jdk会主动调用一次System.gc();

在reserveMemory方法中,只是先将堆外内存相关的属性设值,但并没有真正的分配内存。

分配内存

在预定堆外内存成功后,jdk会调用unsafe中的方法去做堆外内存分配。

base = unsafe.allocateMemory(size);

复制代码

allocateMemory方法是一个native方法,用于分配堆外内存。

在unsafe.cpp中,可以看到他的源码:

UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory0(JNIEnv *env, jobject unsafe, jlong size)) {

size_t sz = (size_t)size;

sz = align_up(sz, HeapWordSize);

void* x = os::malloc(sz, mtOther);

return addr_to_java(x);

} UNSAFE_END

复制代码

调用了malloc函数去分配内存,并返回地址。

释放堆外内存

我们知道,jvm中java对象是采用gcRoot做可达性分析来确定是否回收的,而堆外内存是与gcRoot不关联的,那如何知道在何时应该回收堆外内存呢?

理想方案是:在对应的DirectByteBuffer对象实例被回收时,同步回收堆外内存。

这时应该有同学想到了finalize方法。这或许是一个java向c群体妥协的一个方法,在对象将要被回收时,由gc调用。看上去有点像c中的析构方法,But,该方法的调用是不可靠的,并不能保证对象在被回收前gc一定会调用该方法。

在jdk中,是采用的虚引用的方式去释放堆外内存。

在DirectByteBuffer的构造方法中,有一行如下代码:

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

复制代码

DirectByteBuffer中的cleaner属性就是一个虚引用。

在Deallocator中,同样是使用unsafe中的native方法来释放堆外内存。

unsafe.freeMemory(address);

address = 0;

Bits.unreserveMemory(size, capacity);```

复制代码UNSAFE_ENTRY(void, Unsafe_FreeMemory0(JNIEnv *env, jobject unsafe, jlong addr)) {

void* p = addr_from_java(addr);

os::free(p);

} UNSAFE_END

复制代码

cleaner的调用点位于Reference类的Reference-handler线程中。

在引用对象的可达性发生变化,引用状态变为pending状态时,会在tryHandlePending方法中判断当前引用是否为Cleaner实例,如果是的话,则调用其clean方法,完成堆外内存回收。

其他

在预定内存时,为什么要主动调用System.gc

既然要调用System.gc,那肯定是想通过触发一次gc操作来回收堆外内存,不过我想先说的是堆外内存不会对gc造成什么影响(这里的System.gc除外),但是堆外内存的回收其实依赖于我们的gc机制,首先我们要知道在java层面和我们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能通过操作DirectByteBuffer对象来间接操作对应的堆外内存了。DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块

参考资料

《自己动手写java虚拟机》

openJdk源码&注释

java 堆外内存_详解Java堆外内存相关推荐

  1. java 配置文件的路径_详解java配置文件的路径问题

    详解java配置文件的路径问题 详解java配置文件的路径问题 各种语言都有自己所支持的配置文件,配置文件中有很多变量是经常改变的.不将程序中的各种变量写死,这样能更方便地脱离程序本身去修改相关变量设 ...

  2. java 线程一直运行状态_详解JAVA 线程-线程的状态有哪些?它是如何工作的?

    线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在. 一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源.更加轻量化,也因 ...

  3. java的自动装箱_详解Java 自动装箱与拆箱的实现原理

    详解Java 自动装箱与拆箱的实现原理 发布于 2020-7-4| 复制链接 本篇文章主要介绍了详解Java 自动装箱与拆箱的实现原理,小妖觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小妖 ...

  4. java使用集合存储过程_详解java调用存储过程并封装成map

    详解java调用存储过程并封装成map 发布于 2020-5-1| 复制链接 摘记: 详解java调用存储过程并封装成map           本文代码中注释写的比较清楚不在单独说明,希望能帮助到大 ...

  5. java同步异步调用_详解java 三种调用机制(同步、回调、异步)

    1:同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,jsPwwCe它是一种单向调用 2:回调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口: 3:异步调用:一种类似消 ...

  6. java注解 源码_详解Java注解教程及自定义注解

    详解Java注解教程及自定义注解 更新时间:2016-02-26 11:47:06   作者:佚名   我要评论(0) Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个 ...

  7. java的注解方式_详解Java注解的实现与使用方法

    详解Java注解的实现与使用方法 Java注解是java5版本发布的,其作用就是节省配置文件,增强代码可读性.在如今各种框架及开发中非常常见,特此说明一下. 如何创建一个注解 每一个自定义的注解都由四 ...

  8. java集合for循环_详解Java中list,set,map的遍历与增强for循环

    详解Java中list,set,map的遍历与增强for循环 Java集合类可分为三大块,分别是从Collection接口延伸出的List.Set和以键值对形式作存储的Map类型集合. 关于增强for ...

  9. java访问本地文件_详解Java读取本地文件并显示在JSP文件中

    详解Java读取本地文件并显示在JSP文件中 当我们初学IMG标签时,我们知道通过设置img标签的src属性,能够在页面中显示想要展示的图片.其中src的值,可以是磁盘目录上的绝对,也可以是项目下的相 ...

  10. java中parent结构_详解java中继承关系类加载顺序问题

    详解java中继承关系类加载顺序问题 实例代码: /** * Created by fei on 2017/5/31. */ public class SonClass extends ParentC ...

最新文章

  1. 关于ie,火狐,谷歌浏览器滚动条的隐藏以及自定义样式
  2. Android实战技巧之十一:Android Studio和Gradle
  3. element ui 红点_element-ui 自定义表单验证 , 但是不出现小红心了
  4. Freemarker日期时间类型
  5. Linux基金会:Linux已经战胜微软
  6. 计算机基础:存储系统知识笔记(二)
  7. 如何用js给图片重置宽_如何用js给老婆每天发情话
  8. 编程语言对比 字面常量
  9. ElasticSearch预警服务-Watcher详解-Schedule配置
  10. 博文视点大讲堂第45期——我们应该向魔兽世界学习什么 圆满结束
  11. linux apache配置虚拟主机,linux环境apache多端口配置虚拟主机的方法
  12. VPX,CompactPCI serial 总线
  13. word文档如何插入目录
  14. Scrapy添加headers
  15. 聚观早报 | 吉利正式收购魅族科技;雷军:对标iPhone不是口号
  16. 人工智能基础——什么是人工智能
  17. java计算机毕业设计ssm兴发农家乐服务管理系统n159q(附源码、数据库)
  18. NOIP2017初赛试题
  19. Mac下通过proxychains-ng配置thunderbird来访问gmail
  20. linux中的.rc文件介绍

热门文章

  1. android无线内网打印机打印出图片
  2. python c++情侣网名是什么意思_Python 与 C/C++ 交互的几种方式
  3. 纯手工开发的网站如何快速对接CMS系统
  4. iphone和mac互传文件_DeskConnect,iOS 和 Mac 间的免费文件传输工具 | App+1
  5. 安装WinPE到移动硬盘隐藏分区菜鸟篇(USB-HDD启动方式)
  6. c++ 编一程序,输入一行字符串,将其中的大写英文字母改为小写,再输出。
  7. 头条新闻(Vue实战项目)-首页1
  8. 恢复Foxmail和Outlook邮箱中已删除的邮件
  9. PHP中的四舍五入取整,向上取整,向下取整
  10. 只用一年时间成为一个国民级APP,淘特做对了什么?