JVM堆外内存源码完全解读分析

概述

广义的堆外内存

说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我们在jvm参数里通常还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们认识的Java堆的最大值其实是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,因为它们是一起分配的,那么剩下的都可以认为是堆外内存(广义的)了,这些包括了jvm本身在运行过程中分配的内存,codecachejni里分配的内存,DirectByteBuffer分配的内存等等

狭义的堆外内存

而作为java开发者,我们常说的堆外内存溢出了,其实是狭义的堆外内存,这个主要是指java.nio.DirectByteBuffer在创建的时候分配内存,我们这篇文章里也主要是讲狭义的堆外内存,因为它和我们平时碰到的问题比较密切

JDK/JVM里DirectByteBuffer的实现

DirectByteBuffer通常用在通信过程中做缓冲池,在mina,netty等nio框架中屡见不鲜,先来看看JDK里的实现:

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;

}

通过上面的构造函数我们知道,真正的内存分配是使用的Bits.reserveMemory方法

     static void reserveMemory(long size, int cap) {
        synchronized (Bits.class) {
            if (!memoryLimitSet && VM.isBooted()) {
                maxMemory = VM.maxDirectMemory();
                memoryLimitSet = true;
            }
            // -XX:MaxDirectMemorySize limits the total capacity rather than the
            // actual memory usage, which will differ when buffers are page
            // aligned.
            if (cap <= maxMemory - totalCapacity) {
                reservedMemory += size;
                totalCapacity += cap;
                count++;
                return;
            }
        }

        System.gc();
        try {
            Thread.sleep(100);
        } catch (InterruptedException x) {
            // Restore interrupt status
            Thread.currentThread().interrupt();
        }
        synchronized (Bits.class) {
            if (totalCapacity + cap > maxMemory)
                throw new OutOfMemoryError("Direct buffer memory");
            reservedMemory += size;
            totalCapacity += cap;
            count++;
        }

    }

通过上面的代码我们知道可以通过-XX:MaxDirectMemorySize来指定最大的堆外内存,那么我们首先引入两个问题

  • 堆外内存默认是多大
  • 为什么要主动调用System.gc()

堆外内存默认是多大

如果我们没有通过-XX:MaxDirectMemorySize来指定最大的堆外内存,那么默认的最大堆外内存是多少呢,我们还是通过代码来分析

上面的代码里我们看到调用了sun.misc.VM.maxDirectMemory()

 private static long directMemory = 64 * 1024 * 1024;

    // Returns the maximum amount of allocatable direct buffer memory.
    // The directMemory variable is initialized during system initialization
    // in the saveAndRemoveProperties method.
    //
    public static long maxDirectMemory() {
        return directMemory;
    }

看到上面的代码之后是不是误以为默认的最大值是64M?其实不是的,说到这个值得从java.lang.System这个类的初始化说起

 /**
     * Initialize the system class.  Called after thread initialization.
     */
    private static void initializeSystemClass() {

        // VM might invoke JNU_NewStringPlatform() to set those encoding
        // sensitive properties (user.home, user.name, boot.class.path, etc.)
        // during "props" initialization, in which it may need access, via
        // System.getProperty(), to the related system encoding property that
        // have been initialized (put into "props") at early stage of the
        // initialization. So make sure the "props" is available at the
        // very beginning of the initialization and all system properties to
        // be put into it directly.
        props = new Properties();
        initProperties(props);  // initialized by the VM

        // There are certain system configurations that may be controlled by
        // VM options such as the maximum amount of direct memory and
        // Integer cache size used to support the object identity semantics
        // of autoboxing.  Typically, the library will obtain these values
        // from the properties set by the VM.  If the properties are for
        // internal implementation use only, these properties should be
        // removed from the system properties.
        //
        // See java.lang.Integer.IntegerCache and the
        // sun.misc.VM.saveAndRemoveProperties method for example.
        //
        // Save a private copy of the system properties object that
        // can only be accessed by the internal implementation.  Remove
        // certain system properties that are not intended for public access.
        sun.misc.VM.saveAndRemoveProperties(props);

       ......

        sun.misc.VM.booted();
    }

上面这个方法在jvm启动的时候对System这个类做初始化的时候执行的,因此执行时间非常早,我们看到里面调用了sun.misc.VM.saveAndRemoveProperties(props)

     public static void saveAndRemoveProperties(Properties props) {
        if (booted)
            throw new IllegalStateException("System initialization has completed");

        savedProps.putAll(props);

        // Set the maximum amount of direct memory.  This value is controlled
        // by the vm option -XX:MaxDirectMemorySize=<size>.
        // The maximum amount of allocatable direct buffer memory (in bytes)
        // from the system property sun.nio.MaxDirectMemorySize set by the VM.
        // The system property will be removed.
        String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
        if (s != null) {
            if (s.equals("-1")) {
                // -XX:MaxDirectMemorySize not given, take default
                directMemory = Runtime.getRuntime().maxMemory();
            } else {
                long l = Long.parseLong(s);
                if (l > -1)
                    directMemory = l;
            }
        }

        // Check if direct buffers should be page aligned
        s = (String)props.remove("sun.nio.PageAlignDirectMemory");
        if ("true".equals(s))
            pageAlignDirectMemory = true;

        // Set a boolean to determine whether ClassLoader.loadClass accepts
        // array syntax.  This value is controlled by the system property
        // "sun.lang.ClassLoader.allowArraySyntax".
        s = props.getProperty("sun.lang.ClassLoader.allowArraySyntax");
        allowArraySyntax = (s == null
                               ? defaultAllowArraySyntax
                               : Boolean.parseBoolean(s));

        // Remove other private system properties
        // used by java.lang.Integer.IntegerCache
        props.remove("java.lang.Integer.IntegerCache.high");

        // used by java.util.zip.ZipFile
        props.remove("sun.zip.disableMemoryMapping");

        // used by sun.launcher.LauncherHelper
        props.remove("sun.java.launcher.diag");
    }

如果我们通过-Dsun.nio.MaxDirectMemorySize指定了这个属性,只要它不等于-1,那效果和加了-XX:MaxDirectMemorySize一样的,如果两个参数都没指定,那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法

JNIEXPORT jlong JNICALL
Java_java_lang_Runtime_maxMemory(JNIEnv *env, jobject this)
{
    return JVM_MaxMemory();
}

JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
  JVMWrapper("JVM_MaxMemory");
  size_t n = Universe::heap()->max_capacity();
  return convert_size_t_to_jlong(n);
JVM_END

其中在我们使用CMS GC的情况下的实现如下,其实是新生代的最大值-一个survivor的大小+老生代的最大值,也就是我们设置的-Xmx的值里除去一个survivor的大小就是默认的堆外内存的大小了

size_t GenCollectedHeap::max_capacity() const {
  size_t res = 0;
  for (int i = 0; i < _n_gens; i++) {
    res += _gens[i]->max_capacity();
  }
  return res;
}

size_t DefNewGeneration::max_capacity() const {
  const size_t alignment = GenCollectedHeap::heap()->collector_policy()->min_alignment();
  const size_t reserved_bytes = reserved().byte_size();
  return reserved_bytes - compute_survivor_size(reserved_bytes, alignment);
}

size_t Generation::max_capacity() const {
  return reserved().byte_size();
}

为什么要主动调用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的一个子类,在最终的处理里会通过Unsafefree接口来释放DirectByteBuffer对应的堆外内存块

JDK里ReferenceHandler的实现:

 private static class ReferenceHandler extends Thread {

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            for (;;) {

                Reference r;
                synchronized (lock) {
                    if (pending != null) {
                        r = pending;
                        Reference rn = r.next;
                        pending = (rn == r) ? null : rn;
                        r.next = r;
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }

                // Fast path for cleaners
                if (r instanceof Cleaner) {
                    ((Cleaner)r).clean();
                    continue;
                }

                ReferenceQueue q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }

可见如果pending为空的时候,会通过lock.wait()一直等在那里,其中唤醒的动作是在jvm里做的,当gc完成之后会调用如下的方法VM_GC_Operation::doit_epilogue(),在方法末尾会调用lock的notify操作,至于pending队列什么时候将引用放进去的,其实是在gc的引用处理逻辑中放进去的,针对引用的处理后面可以专门写篇文章来介绍

void VM_GC_Operation::doit_epilogue() {
  assert(Thread::current()->is_Java_thread(), "just checking");
  // Release the Heap_lock first.
  SharedHeap* sh = SharedHeap::heap();
  if (sh != NULL) sh->_thread_holds_heap_lock_for_gc = false;
  Heap_lock->unlock();
  release_and_notify_pending_list_lock();
}

void VM_GC_Operation::release_and_notify_pending_list_lock() {
instanceRefKlass::release_and_notify_pending_list_lock(&_pending_list_basic_lock);
}

对于System.gc的实现,之前写了一篇文章来重点介绍,jvm原理之SystemGC源码分析,它会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存,我们dump内存发现DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为『冰山对象』,我们做ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题,如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc)。

为什么要使用堆外内存

DirectByteBuffer在创建的时候会通过Unsafe的native方法来直接使用malloc分配一块内存,这块内存是heap之外的,那么自然也不会对gc造成什么影响(System.gc除外),因为gc耗时的操作主要是操作heap之内的对象,对这块内存的操作也是直接通过Unsafe的native方法来操作的,相当于DirectByteBuffer仅仅是一个壳,还有我们通信过程中如果数据是在Heap里的,最终也还是会copy一份到堆外,然后再进行发送,所以为什么不直接使用堆外内存呢。对于需要频繁操作的内存,并且仅仅是临时存在一会的,都建议使用堆外内存,并且做成缓冲池,不断循环利用这块内存。

为什么不能大面积使用堆外内存

如果我们大面积使用堆外内存并且没有限制,那迟早会导致内存溢出,毕竟程序是跑在一台资源受限的机器上,因为这块内存的回收不是你直接能控制的,当然你可以通过别的一些途径,比如反射,直接使用Unsafe接口等,但是这些务必给你带来了一些烦恼,Java与生俱来的优势被你完全抛弃了—开发不需要关注内存的回收,由gc算法自动去实现。另外上面的gc机制与堆外内存的关系也说了,如果一直触发不了cms gc或者full gc,那么后果可能很严重。

以上就是JVM堆外内存源码完全解读分析的详细内容,更多关于JVM堆外内存源码分析的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java虚拟机内存区域划分详解

    在谈 JVM 内存区域划分之前,我们先来看一下 Java 程序的具体执行过程,我画了一幅图. Java 源代码文件经过编译器编译后生成字节码文件,然后交给 JVM 的类加载器,加载完毕后,交给执行引擎执行.在整个执行的过程中,JVM 会用一块空间来存储程序执行期间需要用到的数据,这块空间一般被称为运行时数据区,也就是常说的 JVM 内存. 所以,当我们在谈 JVM 内存区域划分的时候,其实谈的就是这块空间--运行时数据区. 大家应该对官方出品的<Java 虚拟机规范>有所了解吧?了解这个规范可

  • 快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互:这些现象多半是由于gc引起. GC时的Stop the World(STW)是大家最大的敌人.但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象. JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation

  • JVM执行引擎和垃圾回收要点总结

    目录 一.执行引擎 二.垃圾对象标记 1.引用计数法 2.可达性分析算法 三.垃圾回收机制 1.标记清除算法 2.标记整理算法 3.复制算法 4.分代收集算法 四.源代码地址 一.执行引擎 应用程序经过编译,转换为字节码文件,字节码加载到内存空间并不能直接在操作系统上执行,执行引擎作为Java虚拟机核心的组成部分,作用就是将字节码指令解释/编译为对应系统平台上的本地机器指令. 解释器:虚拟机启动时会根据预定义对字节码采用逐行解释的方式执行,将每条字节码文件中的内容解释为对应系统平台的本地机器指令

  • JVM入门之JVM内存结构内容详解

    一.java代码编译执行过程 源码编译:通过Java源码编译器将Java代码编译成JVM字节码(.class文件) 类加载:通过ClassLoader及其子类来完成JVM的类加载 类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行   注:Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,   用Java语言编写并编译的程序可以运行在这个平台上 二.JVM简介 1.java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然

  • JVM堆外内存源码完全解读分析

    概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我们在jvm参数里通常还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们认识的Java堆的最大值其实是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,因为它们是一起分配的,那么剩下的都可以认为是堆外内存

  • 浅谈Java堆外内存之突破JVM枷锁

    对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM给的内存不够用,怎么办? 此时,堆外内存登场!利用堆外内存,不仅可以随意操控内存,还能提高网络交互的速度. 背景1:JVM内存的分配 对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下: 新生代:一般来说新创建的对象都分配在这里. 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里

  • JVM分配和回收堆外内存的方式与注意点

    目录 JVM内存模型 如何分配堆外内存 第一种方式:ByteBuffer#allocateDirect 第二种方式:Unsafe#allocateMemory 如何回收堆外内存 第一种方式:Unsafe#freeMemory 第二种方式:JVM回收堆外内存 注意点 注意点1: 注意点2: 引用 总结 JVM内存模型 在JVM中内存被分成两大块,分别是堆内存和堆外内存,堆内存就是JVM使用的内存,而堆外内存就是非JVM使用的内存,一般是分配给机器使用的内存. 那么整个内存模型如下: 因此在JVM中

  • jvm原理之SystemGC源码分析

    概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可以通过jmap来触发等,针对每个场景其实我们都可以写篇文章来做一个介绍,本文重点介绍下System.gc的原理 或许大家已经知道如下相关的知识 system.gc其实是做一次full gc system.gc会暂停整个进程 system.gc一般情况下我们要禁掉,使用-XX:+DisableExplicitGC system.gc在cms

  • JDK源码白话解读之ThreadLocal篇

    目录 引言 ThreadLocal是什么?它能干什么? ThreadLocal实现线程隔离的秘密 为什么ThreadLocal会出现OOM的问题? 内存泄漏演示 内存泄漏问题分析 父子线程的参数传递 总结 引言 因此本文主要结合常见的一些疑问.ThreadLocal源码.应用实例以注意事项来全面而深入地再详细讲解一遍ThreadLocal.希望大家看完本文后可以彻底掌握ThreadLocal. ThreadLocal是什么?它能干什么? 在阐述ThreadLocal之前,我们先来看下它的设计者是

  • 详细总结Java堆栈内存、堆外内存、零拷贝浅析与代码实现

    一.堆栈内存 堆栈内存,顾名思义,指的是堆内存以及栈内存,其中,堆内存是由Java GC进行管理的内存区域,而栈内存则是线程内存.关于栈内存,这里不去细说.以Hotspot为例,堆内存的简要结构如下图所示: 而堆栈的关系,我们可以通过一行简单的代码来理解: public static void main(String[] args) { Object o = new Object(); } 上述代码主要完成了两件事,new Object( ) 在堆上开辟了一块内存,也就是说,new Object

  • Java CopyOnWriteArrayList源码超详细分析

    目录 一.概述 二.类图 三.核心方法 1.add() 2.set() 3.remove() 4.get() 5.size() 四.总结 一.概述 CopyOnWriteArrayList是基于写时复制技术实现的,适用于读多写少场景下的线程安全的并发容器.读操作永远不会加锁,读读.读写都不会冲突,只有写写需要等待.写操作时,为了不影响其它线程的读取,它会进行一次自我复制,待数据写入完成后再替换array数组.array数组是被volatile修饰的,它被修改后可以被其他线程立刻发现. publi

  • Android用于加载xml的LayoutInflater源码超详细分析

    1.在view的加载和绘制流程中:文章链接 我们知道,定义在layout.xml布局中的view是通过LayoutInflate加载并解析成Java中对应的View对象的.那么具体的解析过程是哪样的. 先看onCreate方法,如果我们的Activity是继承自AppCompactActivity.android是通过getDelegate返回的对象setContentView,这个mDelegate 是AppCompatDelegateImpl的实例. @Override protected

  • python SocketServer源码深入解读

    再看继承 真正的大餐来之前,还是来点儿开胃菜!回顾一下关于类的继承的知识: 我们先看上面的代码,这是一个简单的类继承,我们可以看到父类Base和子类Son,它们中各有一个Testfunc方法,当我们实例化子类的对象sonobj时,可以看到初始化方法中黄色框框调用了Testfunc,那么这个时候执行的是哪个类里面的代码呢?我会告诉你执行的是子类里面的方法,不信就自己试试吧,代码在下面~ 为什么呢?其实这里是绕了一个圈,所以把很多人绕晕了,包括我!后来想想其实很容易,我们看最右边的图: 如果这样看,

  • Android 2.3 拨号上网流程从源码角度进行分析

    通常,如果我们想使用SIM卡拨号上网功能,我们要在设置中进行简单的配置,步骤如下: 设置 ->无线和网络 ->移动网络 ->(已启用数据/数据漫游/接入点名称/仅使用2G网络/网络运营商) 我们必须选中其中的"已启用数据"选项,然后配置接入点名称后就可以上网了,当然有的设置中已经根据你的SIM卡类型默认设置了接入点,这时候你只选择"已启用数据"项后就可以完成上网功能设置. 这些设置步骤究竟做了哪些事情呢?我们现在就从源码的角度进行分析. 1. 首先

随机推荐