从0到1起步-跟我进入堆外内存的奇妙世界

一、什么是堆外内存

1、堆内内存(on-heap memory

回顾堆外内存和堆内内存是相对的二个概念,其中堆内内存是我们平常工作中接触比较多的,我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式:

堆内内存 = 新生代+老年代+持久代

如下面的图所示:

在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。

常见的垃圾回收算法主要有:

  • 引用计数器法(Reference Counting)
  • 标记清除法(Mark-Sweep)
  • 复制算法(Coping)
  • 标记压缩法(Mark-Compact)
  • 分代算法(Generational Collecting)
  • 分区算法(Region)

注:在这里我们不对各个算法进行深入介绍,感兴趣的同学可以关注我的下一篇关于垃圾回收算法的介绍分享。

2、堆外内存(off-heap memory)

和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。

作为JAVA开发者我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。

DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作,下面介绍构造方法

DirectByteBuffer(int cap) {                 

        super(-1, 0, cap, cap);

        //内存是否按页分配对齐

        boolean pa = VM.isDirectMemoryPageAligned();

        //获取每页内存大小

        int ps = Bits.pageSize();

        //分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量

        long size = Math.max(1L, (long)cap + (pa ? ps : 0));

        //用Bits类保存总分配内存(按页分配)的大小和实际内存的大小

        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;

    }

注:在Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。

private static class Deallocator implements Runnable  {

    private static Unsafe unsafe = Unsafe.getUnsafe();

    private long address;

    private long size;

    private int capacity;

    private Deallocator(long address, long size, int capacity) {

        assert (address != 0);

        this.address = address;

        this.size = size;

        this.capacity = capacity;

    }

    public void run() {

        if (address == 0) {

            // Paranoia

            return;

        }

        unsafe.freeMemory(address);

        address = 0;

        Bits.unreserveMemory(size, capacity);

    }

}

二、使用堆外内存的优点

  1. 减少了垃圾回收

    因为垃圾回收会暂停其他的工作。

  2. 加快了复制的速度

    堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。

同样任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

三、使用DirectByteBuffer的注意事项

java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。

四、DirectByteBuffer使用测试

我们在写NIO程序经常使用ByteBuffer来读取或者写入数据,那么使用ByteBuffer.allocate(capability)还是使用ByteBuffer.allocteDirect(capability)来分配缓存了?第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢;第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。

代码如下:

package com.stevex.app.nio;

import java.nio.ByteBuffer;

import java.util.concurrent.TimeUnit;

public class DirectByteBufferTest {

    public static void main(String[] args) throws InterruptedException{

            //分配128MB直接内存

        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);

        TimeUnit.SECONDS.sleep(10);

        System.out.println("ok");

    }

}

测试用例1:设置JVM参数-Xmx100m,运行异常,因为如果没设置-XX:MaxDirectMemorySize,则默认与-Xmx参数值相同,分配128M直接内存超出限制范围。

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

    at java.nio.Bits.reserveMemory(Bits.java:658)

    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)

    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)

    at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)

测试用例2:设置JVM参数-Xmx256m,运行正常,因为128M小于256M,属于范围内分配。

测试用例3:设置JVM参数-Xmx256m -XX:MaxDirectMemorySize=100M,运行异常,分配的直接内存128M超过限定的100M。

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

    at java.nio.Bits.reserveMemory(Bits.java:658)

    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)

    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)

    at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)

测试用例4:设置JVM参数-Xmx768m,运行程序观察内存使用变化,会发现clean()后内存马上下降,说明使用clean()方法能有效及时回收直接缓存。代码如下:

package com.stevex.app.nio;

import java.nio.ByteBuffer;

import java.util.concurrent.TimeUnit;

import sun.nio.ch.DirectBuffer;

public class DirectByteBufferTest {

    public static void main(String[] args) throws InterruptedException{

        //分配512MB直接缓存

        ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*512);

        TimeUnit.SECONDS.sleep(10);

        //清除直接缓存

        ((DirectBuffer)bb).cleaner().clean();

        TimeUnit.SECONDS.sleep(10);

        System.out.println("ok");

    }

}

五、细说System.gc方法

1、JDK里的System.gc的实现

/**

 * Runs the garbage collector.

 * <p>

 * Calling the <code>gc</code> method suggests that the Java Virtual

 * Machine expend effort toward recycling unused objects in order to

 * make the memory they currently occupy available for quick reuse.

 * When control returns from the method call, the Java Virtual

 * Machine has made a best effort to reclaim space from all discarded

 * objects.

 * <p>

 * The call <code>System.gc()</code> is effectively equivalent to the

 * call:

 * <blockquote><pre>

 * Runtime.getRuntime().gc()

 * </pre></blockquote>

 *

 * @see     java.lang.Runtime#gc()

 */

public static void gc() {

    Runtime.getRuntime().gc();

}

其实发现System.gc方法其实是调用的Runtime.getRuntime.gc(),我们再接着看。

/*

  运行垃圾收集器。

调用此方法表明,java虚拟机扩展

努力回收未使用的对象,以便内存可以快速复用,

当控制从方法调用返回的时候,虚拟机尽力回收被丢弃的对象

*/

public native void gc();

这里看到gc方法是native的,在java层面只能到此结束了,代码只有这么多,要了解更多,可以看方法上面的注释,不过我们需要更深层次地来了解其实现,那还是准备好进入到jvm里去看看。

2、System.gc的作用有哪些说起堆外内存免不了要提及System.gc方法,下面就是使用了System.gc的作用是什么?

  • 做一次full gc
  • 执行后会暂停整个进程。
  • System.gc我们可以禁掉,使用-XX:+DisableExplicitGC,其实一般在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一点的gc,也就是并行gc。
  • 最常见的场景是RMI/NIO下的堆外内存分配等

注:如果我们使用了堆外内存,并且用了DisableExplicitGC设置为true,那么就是禁止使用System.gc,这样堆外内存将无从触发极有可能造成内存溢出错误,在这种情况下可以考虑使用ExplicitGCInvokesConcurrent参数。

说起Full gc我们最先想到的就是stop thd world,这里要先提到VMThread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了。

六、开源堆外缓存框架

关于堆外缓存的开源实现。查询了一些资料后了解到的主要有:

  • Ehcache 3.0:3.0基于其商业公司一个非开源的堆外组件的实现。
  • Chronical Map:OpenHFT包括很多类库,使用这些类库很少产生垃圾,并且应用程序使用这些类库后也很少发生Minor GC。类库主要包括:Chronicle Map,Chronicle Queue等等。
  • OHC:来源于Cassandra 3.0, Apache v2。
  • Ignite: 一个规模宏大的内存计算框架,属于Apache项目。

本文转载自微信公众号 中生代技术 freshmanTechnology

文/小程故事多(简书作者)

原文链接:http://www.jianshu.com/p/50be08b54bee

时间: 2024-10-30 23:41:05

从0到1起步-跟我进入堆外内存的奇妙世界的相关文章

请问c语言中,int a=0;变量名a和数值是怎么在内存分配的

问题描述 请问c语言中,int a=0;变量名a和数值是怎么在内存分配的 我看了很多答案,有些说只分配一个内存存0,a被隐藏了那么这个隐藏又是怎么个隐藏法呢还有说a和0同时存着不懂,求指教 解决方案 每次我们要使用某变量时都要事先这样声明它,它其实是内存中申请了一个名为i的整型变量宽度的空间(DOS下的16位编程中其宽度为2个字节),和一个名为a的字符型变量宽度的空间(占1个字节).i在内存起始地址为6上申请了两个字节的空间(我这里假设了int的宽度为16位,不同系统中int的宽度可能是不一样的

PostgreSQL 10.0 preview 性能增强 - (多维分析)更快,更省内存hashed aggregation with grouping sets

标签 PostgreSQL , 10.0 , hashed aggregation with grouping sets 背景 grouping sets 是多维分析语法,PostgreSQL 从9.5开始支持这种语法,常被用于OLAP系统,数据透视等应用场景. <PostgreSQL 9.5 new feature - Support GROUPING SETS, CUBE and ROLLUP.> 由于多维分析的一个QUERY涉及多个GROUP,所以如果使用hash agg的话,需要多个H

Cassandra杀回数据库排行Top 10,及需求Java 7的2.0版本

在MySQL被Oracle收购后,业内对这个开源数据库的议论就从未停止,对于PostgreSQL将取而代之成为最人气开源数据库的声音也从未停歇.然而从DB-Engines排行榜来看,PostgreSQL与MySQL之间的差距远不只是"几层楼"那么高(PostgreSQ得分不到MySQL的零头).着眼整个排行榜上的193个数据库,我们会发现NoSQL数据库已占大半江山,榜上有名的传统关系型数据库管理系统仅余75个. 上图为9月份最新的排行,在Oracle稳坐头把交椅的同时,MySQL与M

Flink 原理与实现:内存管理

如今,大数据领域的开源框架(Hadoop,Spark,Storm)都使用的 JVM,当然也包括 Flink.基于 JVM 的数据分析引擎都需要面对将大量数据存到内存中,这就不得不面对 JVM 存在的几个问题: Java 对象存储密度低.一个只包含 boolean 属性的对象占用了16个字节内存:对象头占了8个,boolean 属性占了1个,对齐填充占了7个.而实际上只需要一个bit(1/8字节)就够了. Full GC 会极大地影响性能,尤其是为了处理更大数据而开了很大内存空间的JVM来说,GC

Netty源码解读(二)Netty中的buffer

感谢网友[黄亿华]投递本稿. 上一篇文章我们概要介绍了Netty的原理及结构,下面几篇文章我们开始对Netty的各个模块进行比较详细的分析.Netty的结构最底层是buffer模块,这部分也相对独立,我们就先从buffer讲起. What: buffer二三事 buffer中文名又叫缓冲区,按照维基百科的解释,是"在数据传输时,在内存里开辟的一块临时保存数据的区域".它其实是一种化同步为异步的机制,可以解决数据传输的速率不对等以及不稳定的问题. 根据这个定义,我们可以知道涉及I/O(特

杨彪 | 一次线上游戏卡死的解决历程(文末赠书福利)

题图:StartupStock@Pixabay 编辑:冷锋 作者:杨彪 本文首发于简书云时代构架杨彪 http://www.jianshu.com/p/7885bbf153f5 事故的发生详细过程 故事是发生在几个月前的线上真实案例,我将在本文中以故事形式为大家还原这次解决游戏卡死的经历过程,其中有很多线上实战经验和技巧都值得分享借鉴的,也有作者自创的处理线上问题"四部曲"--望问闻切,还有最经典的"甩锅"秘诀. 不管白猫黑猫,能立马解决线上问题的就是好猫,线上问题

Flink运行时之基于Netty的网络通信上

概述 本文以及接下来的几篇文章将介绍Flink运行时TaskManager间进行数据交换的核心部分--基于Netty通信框架远程请求ResultSubpartition.作为系列文章的第一篇,先列出一些需要了解的基础对象. NettyConnectionManager Netty连接管理器(NettyConnectionManager)是连接管理器接口(ConnectionManager)针对基于Netty的远程连接管理的实现者.它是TaskManager中负责网络通信的网络环境对象(Netwo

Java Magic. Part 4: sun.misc.Unsafe

原文地址 译文地址 译者:许巧辉 校对:梁海舰 Java是一门安全的编程语言,防止程序员犯很多愚蠢的错误,它们大部分是基于内存管理的.但是,有一种方式可以有意的执行一些不安全.容易犯错的操作,那就是使用Unsafe类. 本文是sun.misc.Unsafe公共API的简要概述,及其一些有趣的用法. Unsafe 实例 在使用Unsafe之前,我们需要创建Unsafe对象的实例.这并不像Unsafe unsafe = new Unsafe()这么简单,因为Unsafe的构造器是私有的.它也有一个静

RocketMQ 迈入50万TPS消息俱乐部

前言 消息团队一直致力于RocketMQ的性能优化,双十一前进行了低延时(毛刺)优化,保障了双十一万亿消息的流转如丝般顺滑,在2016年双十一种,MetaQ以接近万亿的消息总量支撑着全集团数千个应用,在系统解耦.削峰填谷.数据库同步.位点回滚消费等多种业务场景中,MetaQ都有精彩.稳定的表现.高可用低延迟,高并发抗堆积,2016双11的MetaQ真正做到了如丝般顺滑. 而最近通过对性能的持续优化,在RocketMQ最新的中小消息TPS已达47万,本人在F43机型上自测时TPS峰值为57万TPS