精华内容
下载资源
问答
  • 学习Java的小伙伴在学习Java虚拟机运行时数据区中学习过堆和直接内存,其实这里的堆和直接内存分别就对应着堆内内存和堆外内存,这篇文章就重点介绍堆外内存,Java程序是如何使用堆外内存的等一系列问题。...

    学习Java的小伙伴在学习Java虚拟机运行时数据区中学习过堆和直接内存,其实这里的堆和直接内存分别就对应着堆内内存和堆外内存,这篇文章就重点介绍堆外内存,Java程序是如何使用堆外内存的等一系列问题。

    一、堆内内存(on-heap memory)

    堆内内存就是我们日常说的堆,堆内内存 = 新生代+老年代+持久代。堆内内存完全遵循JVM虚拟机的内存管理机制,采用垃圾收集器(GC)统一进行内存管理。
    注意:JDK8中已经没有持久代了。

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

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

    1.堆外内存的特点
    (1)对于大内存有良好的伸缩性
    (2)对垃圾回收停顿的改善可以明显感觉到
    (3)在进程间可以共享,减少虚拟机间的复制

    2.什么情况下使用堆外内存?
    (1)堆外内存适用于生命周期中等或较长的对象。( 如果是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用造成的性能影响 )。
    (2)直接的文件拷贝操作,或者I/O操作。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操作,因为I/O操作是系统内核内存和设备间的通信,而不是通过程序直接和外设通信的。
    (3)同时,还可以使用池+堆外内存的组合方式,来对生命周期较短,但涉及到I/O操作的对象进行堆外内存的再使用。(Netty中就使用了该方式)

    3.使用堆外内存的优缺点:

    (1)优点:
    1)减少了垃圾回收
      因为垃圾回收会暂停其他的工作,这就是STW(Stop-The-World)机制了。
    注意:
    关于STW机制可参考我另一篇文章《Java GC的疑问解答(面试点归纳)》中的第18条。
    2)加快了复制的速度
      堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送;而堆外内存相当于省略掉了这个工作。
      
    (2)缺点:
      内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

    4.堆外内存与内存池的比较
    (1)内存池
    主要用于两类对象:
    生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;
    加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。
    (2)堆外内存
    它和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的

    5.DirectByteBuffer和ByteBuffer对比理解?
    DirectByteBuffer是ByteBuffer的一个子类。为什么会说ByteBuffer的性能低下呢?因为在使用ByteBuffer进行I/O操作时会执行以下操作:
    (1)将堆内存中缓冲区数据拷贝到临时缓冲区
    (2)对临时缓冲区的数据执行低层次I/O操作
    (3)临时缓冲区对象离开作用域,并最终被回收成为无用数据
    与之相对,DirectByteBuffer由于将内存分配在了堆外内存因此可以直接执行较低层次的I/O操作数据,减少了拷贝次数因此也获得了较高的性能。

    5.java.nio.DirectByteBuffer-直接缓冲
      我们可以用DirectByteBuffer对象进行堆外内存的管理使用,它会在对象创建的时候就分配堆外内存。DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作。

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

    (3)DirectByteBuffer内存申请与回收
    可参考:
    https://blog.csdn.net/dnuvb68642/article/details/102248897

    1)内存申请
    在构造DirectByteBuffer时就已经执行了内存申请操作。
    源码分析:
    DirectByteBuffer.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)) {
                // 四舍五入到页面边界
                address = base + ps - (base & (ps - 1));
            } else {
                address = base;
            }
            //此行代码用于实现当DirectByteBuffer被回收时,堆外内存也会被释放
            cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
            att = null;
        }
    

    Bits.reserveMemory

    	//当分配或释放直接内存时,应该调用这些方法。
    	//它们允许用户控制一个进程可以访问的直接内存的数量。
    	//所有大小都是用字节指定的。
        static void reserveMemory(long size, int cap) {
    
            if (!memoryLimitSet && VM.isBooted()) {
                maxMemory = VM.maxDirectMemory();
                memoryLimitSet = true;
            }
    
    		// 乐观地尝试预定直接内存(DirectMemory)的内存
            // optimist!
            if (tryReserveMemory(size, cap)) {
                return;
            }
    
    		// 如果预定内存失败,则对直接内存中无用的内存执行回收操作
            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
            ///重试,同时帮助排队挂起的引用对象,包括执行挂起的Cleaner(s),其中包括释放直接缓冲区内存的Cleaner(s)
            while (jlra.tryHandlePendingReference()) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
            }
    
    		// 触发VM的Reference处理(Full GC)
            // trigger VM's Reference processing
            System.gc();
    
    		// 执行多次循环,尝试进行内存回收操作,如果多次尝试失败之后,则抛出OutOfMemory异常
            // a retry loop with exponential back-off delays
            // (this gives VM some time to do it's job)
            boolean interrupted = false;
            try {
                long sleepTime = 1;
                int sleeps = 0;
                while (true) {
                    if (tryReserveMemory(size, cap)) {
                        return;
                    }
                    if (sleeps >= MAX_SLEEPS) {
                        break;
                    }
                    if (!jlra.tryHandlePendingReference()) {
                        try {
                            Thread.sleep(sleepTime);
                            sleepTime <<= 1;
                            sleeps++;
                        } catch (InterruptedException e) {
                            interrupted = true;
                        }
                    }
                }
    
                // no luck
                throw new OutOfMemoryError("Direct buffer memory");
    
            } finally {
                if (interrupted) {
                    // 不要吞下中断
                    // don't swallow interrupts
                    Thread.currentThread().interrupt();
                }
            }
        }
    

    此方法的主要功能就是检查当前DirectMemory内存是否足够构建DirectByteBuffer的缓冲区,并通过CAS的方式设置当前已使用的内存。

    Bits.tryReserveMemory

    //尝试预定内存
    private static boolean tryReserveMemory(long size, int cap) {
            // -XX:MaxDirectMemorySize 限制总容量而不是实际的内存使用,当缓冲区页对齐时,实际内存使用会有所不同。
            long totalCap;
         //检查内存是否足够
            while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
           //如果内存足够,则尝试CAS设置totalCapacity 
                if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
                    reservedMemory.addAndGet(size);
                    count.incrementAndGet();
                    return true;
                }
            }
            return false;
        }
    

    2)内存释放
    DirectByteBuffer中的直接内存缓冲区释放的方式有两种:
    1)ReferenceHandler线程会自动检查有无被回收的DirectByteBuffer,如果有则执行Cleaner.clean方法释放其对应的直接内存
    2)通过调用SharedSecrets.getJavaLangRefAccess()方法来释放内存,具体见Reference类代码分析。

    内存释放的关键就在:

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

    梧高凤必至,花香蝶自来。

    展开全文
  • 1、堆内内存概念 堆内一般指堆内内存,英文全称:on-heap memory java虚拟机分配非空对象一般就放到堆内内存,并且虚拟机会定期会进行垃圾回收在某些特定的时间点,它会进行一次彻底的回收(full gc)。full gc时,...

    1、堆内内存概念

    堆内一般指堆内内存,英文全称:on-heap memory

    java虚拟机分配非空对象一般就放到堆内内存,并且虚拟机会定期会进行垃圾回收在某些特定的时间点,它会进行一次彻底的回收(full gc)。full gc时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。所以通过堆外内存来解决该问题

    2、堆内内存认知

    堆内内存 = 新生代+老年代+持久代
    对于JVM,在jvm参数中可使用-Xms,-Xmx等参数就可以设置堆的大小和最大值
    在这里插入图片描述

    • 年轻代 (Young Generation)

    (1)存放的是新生成的对象

    (2)年轻代的目标是尽可能快速的收集掉那些生命周期短的对象

    (3)Eden 大部分对象在Eden区中生成

    当Eden区满时, 依然存活的对象将被复制到Survivor区, 当一个Survivor 区满时, 此区的存活对象将被复制到另外一个Survivor区,Survivor通常2个

    • 老年代 (Old Generation)

    (1)存放了在年轻代中经历了N次垃圾回收后仍存活的对象, 是一些生命周期较长的对象

    • 持久代 (Permanent Generation)

    (1)存放静态文件, 如静态类和方法等。持久代对垃圾回收没有显著影响, 但是有些应用可能动态生成或者调用一些class, 比如Hibernate, Mybatis 等, 此时需要设置一个较大的持久代空间来存放这些运行过程中新增的类。

    (2)设置持久代大小参数: -XX:MaxPermSize= Perm => Permanent

    3、堆外内存

    堆外一般指堆外内存,英文全称:off-heap memory

    堆外内存=物理机内存

    堆外内存指的是java虚拟机堆以外的内存,这个区域是受操作系统管理,而不是jvm。

    4、堆外内存的优点和缺点

    • 使用堆外内存的优点
      (1)减少了垃圾回收机制(FULL GC 会暂停其他的工作)
      (2)加快了复制的速度:堆内在flush到远程时, 会先复制到直接内存(非堆内存), 然后再发送。而堆外内存(本身就是物理机内存)几乎省略了该步骤
    • 使用堆外内存的缺点
      (2)内存难以控制
      使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

    5、垃圾回收(GC)

    • Scavenge GC
      (1)一般当新对象生成并且在Eden申请空间失败时就会触发Scavenger GC, 对Eden区域进行GC, 清除非存活对象, 并且把尚存或的对象移动到Survivor区, 然后整理两个Survivor区。
      (2)该方式的GC是对年轻代的Eden区进行,不会影响到年老代。
      (3)由于大部分对象是从Eden区开始的, 同时Eden区分配的内存不会很大, 所以Eden区的GC会很频繁。
    • Full GC
      (1)对整个堆进行整理, 包括Young, Tenured 和Permanent。
      (2)所消耗的时间较长, 所以要尽量减少 Full GC 的次数

    导致 Full GC 的可能原因:

    (1)老年代(Tenured) 被写满
    (2)持久代(Permanent) 被写满
    (3)System.gc() 被显示调用
    (4)上一次GC之后Heap 的各域分配策略动态变化

    展开全文
  • 导读:Spark作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色。理解Spark内存管理的基本原理,有助于更好地开发Spark应用程序和进行性能调优。本文将...

    导读:Spark作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色。理解Spark内存管理的基本原理,有助于更好地开发Spark应用程序和进行性能调优。本文将详细介绍两部分内容,第一部分介绍Spark堆内和堆外内存的规划,主要包含堆内内存、堆外内存以及内存管理接口等方面;第二部重点介绍Spark内存空间的分配,主要包含静态内存管理与统一内存管理的机制。

    前言


    本文旨在梳理出Spark内存管理的脉络,抛砖引玉,引出读者对这个话题的深入探讨。本文中阐述的原理基于Spark 2.1版本,阅读本文需要读者有一定的Spark和Java基础,了解RDD、Shuffle、JVM等相关概念。

    在执行Spark的应用程序时,Spark集群会启动Driver和Executor两种JVM进程,前者为主控进程,负责创建Spark上下文,提交Spark作业(Job),并将作业转化为计算任务(Task),在各个Executor进程间协调任务的调度,后者负责在工作节点上执行具体的计算任务,并将结果返回给Driver,同时为需要持久化的RDD提供存储功能。由于Driver的内存管理相对来说较为简单,本文主要对Executor的内存管理进行分析,下文中的Spark内存均特指Executor的内存。

    图1 Spark的Driver和Worker

    堆内与堆外内存规划


    作为一个JVM进程,Executor的内存管理建立在JVM的内存管理之上,Spark对JVM的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。

    图2 堆外和堆内内存

    堆内内存

    堆内内存的大小,由Spark应用程序启动时的–executor-memory或spark.executor.memory参数配置。Executor内运行的并发任务共享JVM堆内内存,这些任务在缓存RDD和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行Shuffle时占用的内存被规划为执行(Execution)内存,剩余的部分不做特殊规划,那些Spark内部的对象实例,或者用户定义的Spark应用程序中的对象实例,均占用剩余的空间。不同的管理模式下,这三部分占用的空间大小各不相同(下面会进行介绍)。

    Spark对堆内内存的管理是一种逻辑上的“规划式”的管理,因为对象实例占用内存的申请和释放都由JVM完成,Spark只能在申请后和释放前记录这些内存,我们来看其具体流程:

    • 申请内存:

      • Spark在代码中new一个对象实例

      • JVM从堆内内存分配空间,创建对象并返回对象引用

      • Spark保存该对象的引用,记录该对象占用的内存

    • 释放内存:

      • Spark记录该对象释放的内存,删除该对象的引用

      • 等待JVM的垃圾回收机制释放该对象占用的堆内内存

    我们知道,JVM的对象可以以序列化的方式存储,序列化的过程是将对象转换为二进制字节流,本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储,在访问时则需要进行序列化的逆过程——反序列化,将字节流转化为对象,序列化的方式可以节省存储空间,但增加了存储和读取时候的计算开销。

    对于Spark中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。此外,在被Spark标记为释放的对象实例,很有可能在实际上并没有被JVM回收,导致实际可用的内存小于Spark记录的可用内存。所以Spark并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。

    虽然不能精准控制堆内内存的申请和释放,但Spark通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。

    堆外内存

    为了进一步优化内存的使用以及提高Shuffle时排序的效率,Spark引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。利用JDK Unsafe API(从Spark 2.0开始,在管理堆外的存储内存时不再基于Tachyon,而是与堆外的执行内存一样,基于JDK Unsafe API实现),Spark可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的GC扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。

    在默认情况下堆外内存并不启用,可通过配置spark.memory.offHeap.enabled参数启用,并由spark.memory.offHeap.size参数设定堆外空间的大小。除了没有other空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

    内存管理接口

    Spark为存储内存和执行内存的管理提供了统一的接口——MemoryManager,同一个Executor内的任务都调用这个接口的方法来申请或释放内存:

    //申请存储内存
    def acquireStorageMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean
    //申请展开内存
    def acquireUnrollMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean
    //申请执行内存
    def acquireExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Long
    //释放存储内存
    def releaseStorageMemory(numBytes: Long, memoryMode: MemoryMode): Unit
    //释放执行内存
    def releaseExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Unit
    //释放展开内存
    def releaseUnrollMemory(numBytes: Long, memoryMode: MemoryMode): Unit
    

    我们看到,在调用这些方法时都需要指定内存模式(MemoryMode),这个参数决定了是在堆内还是堆外完成这次操作。

    MemoryManager的具体实现上,Spark 1.6之后默认为统一管理(Unified Memory Manager)方式,1.6之前采用的静态管理(Static Memory Manager)方式仍被保留,可通过配置spark.memory.useLegacyMode参数启用。两种方式的区别在于对空间分配的方式,下面分别对这两种方式进行介绍。

    内存空间分配


    静态内存管理 

    ——堆内

    在Spark最初采用的静态内存管理机制下,存储内存、执行内存和其他内存三部分的大小在Spark应用程序运行期间是固定的,但用户可以在应用程序启动前进行配置,堆内内存的分配如图3所示:

    图3 静态内存管理图示——堆内

    可以看到,可用的堆内内存的大小需要按照下面的方式计算:

    可用的存储内存 = systemMaxMemory * spark.storage.memoryFraction * spark.storage.safetyFraction
    可用的执行内存 = systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction
    

    其中systemMaxMemory取决于当前JVM堆内内存的大小,最后可用的执行内存或者存储内存要在此基础上与各自的memoryFraction参数和safetyFraction参数相乘得出。上述计算公式中的两个safetyFraction参数,其意义在于在逻辑上预留出1-safetyFraction这么一块保险区域,降低因实际内存超出当前预设范围而导致OOM的风险(对于非序列化对象的内存采样估算会产生误差)。值得注意的是,这个预留的保险区域仅仅是一种逻辑上的规划,在具体使用时Spark并没有区别对待,和“其它内存”一样交给了JVM去管理。

    ——堆外

    堆外的空间分配较为简单,存储内存、执行内存的大小同样是固定的,如图4所示。可用的执行内存和存储内存占用的空间大小直接由参数spark.memory.storageFraction决定,由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域。

    图4 静态内存管理图示——堆外

    静态内存管理机制实现起来较为简单,但如果用户不熟悉Spark的存储机制,或没有根据具体的数据规模和计算任务或做相应的配置,很容易造成“一半海水,一半火焰”的局面,即存储内存和执行内存中的一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容。由于新的内存管理机制的出现,这种方式目前已经很少有开发者使用,出于兼容旧版本的应用程序的目的,Spark仍然保留了它的实现。

    统一内存管理

    Spark 1.6之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,如图5和图6所示

    图5 统一内存管理图示——堆内

    图6 统一内存管理图示——堆外

    其中最重要的优化在于动态占用机制,其规则如下:

    • 设定基本的存储内存和执行内存区域(spark.storage.storageFraction参数),该设定确定了双方各自拥有的空间的范围。

    • 双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的Block)。

    • 执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后“归还”借用的空间。

    • 存储内存的空间被对方占用后,无法让对方“归还”,因为需要考虑Shuffle过程中的很多因素,实现起来较为复杂。

    图7 动态占用机制图示



    凭借统一内存管理机制,Spark在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护Spark内存的难度,但并不意味着开发者可以高枕无忧。譬如,所以如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能,因为缓存的RDD数据通常都是长期驻留内存的。所以要想充分发挥Spark的性能,需要开发者进一步了解存储内存和执行内存各自的管理方式和实现原理。

    下面这部分主要介绍两部分:存储内存管理,包含RDD的持久化机制、RDD缓存的过程、淘汰和落盘;执行内存管理,包含多任务间内存分配、Shuffle的内存占用。

    存储内存管理


    RDD的持久化机制

    弹性分布式数据集(RDD)作为Spark最根本的数据抽象,是只读的分区记录(Partition)的集合,只能基于在稳定物理存储中的数据集上创建,或者在其他已有的RDD上执行转换(Transformation)操作产生一个新的RDD。转换后的RDD与原始的RDD之间产生的依赖关系,构成了血统(Lineage)。凭借血统,Spark保证了每一个RDD都可以被重新恢复。但RDD的所有转换都是惰性的,即只有当一个返回结果给Driver的行动(Action)发生时,Spark才会创建任务读取RDD,然后真正触发转换的执行。

    Task在启动之初读取一个分区时,会先判断这个分区是否已经被持久化,如果没有则需要检查Checkpoint或按照血统重新计算。所以如果一个RDD上要执行多次行动,可以在第一次行动中使用persist或cache方法,在内存或磁盘中持久化或缓存这个RDD,从而在后面的行动时提升计算速度。事实上,cache方法是使用默认的MEMORY_ONLY的存储级别将RDD持久化到内存,故缓存是一种特殊的持久化。堆内和堆外存储内存的设计,便可以对缓存RDD时使用的内存做统一的规划和管理(存储内存的其他应用场景,如缓存broadcast数据,暂时不在本文的讨论范围之内)。

    RDD的持久化由Spark的Storage模块负责,实现了RDD与物理存储的解耦合。Storage模块负责管理Spark在计算过程中产生的数据,将那些在内存或磁盘、在本地或远程存取数据的功能封装了起来。在具体实现时Driver端和Executor端的Storage模块构成了主从式的架构,即Driver端的BlockManager为Master,Executor端的BlockManager为Slave。Storage模块在逻辑上以Block为基本存储单位,RDD的每个Partition经过处理后唯一对应一个Block(BlockId的格式为rdd_RDD-ID_PARTITION-ID)。Master负责整个Spark应用程序的Block的元数据信息的管理和维护,而Slave需要将Block的更新等状态上报到Master,同时接收Master的命令,例如新增或删除一个RDD。

    图8 Storage模块示意图

    在对RDD持久化时,Spark规定了MEMORY_ONLY、MEMORY_AND_DISK等7种不同的存储级别,而存储级别是以下5个变量的组合:

    class StorageLevel private(
        private var _useDisk: Boolean, //磁盘
        private var _useMemory: Boolean, //这里其实是指堆内内存
        private var _useOffHeap: Boolean, //堆外内存
        private var _deserialized: Boolean, //是否为非序列化
        private var _replication: Int = 1 //副本个数
    )
    

    通过对数据结构的分析,可以看出存储级别从三个维度定义了RDD的Partition(同时也就是Block)的存储方式:

    • 存储位置:磁盘/堆内内存/堆外内存。如MEMORY_AND_DISK是同时在磁盘和堆内内存上存储,实现了冗余备份。OFF_HEAP则是只在堆外内存存储,目前选择堆外内存时不能同时存储到其他位置。

    • 存储形式:Block缓存到存储内存后,是否为非序列化的形式。如MEMORY_ONLY是非序列化方式存储,OFF_HEAP是序列化方式存储。

    • 副本数量:大于1时需要远程冗余备份到其他节点。如DISK_ONLY_2需要远程备份1个副本。

    RDD缓存的过程

    RDD在缓存到存储内存之前,Partition中的数据一般以迭代器(Iterator)的数据结构来访问,这是Scala语言中一种遍历数据集合的方法。通过Iterator可以获取分区中每一条序列化或者非序列化的数据项(Record),这些Record的对象实例在逻辑上占用了JVM堆内内存的other部分的空间,同一Partition的不同Record的空间并不连续。

    RDD在缓存到存储内存之后,Partition被转换成Block,Record在堆内或堆外存储内存中占用一块连续的空间。将Partition由不连续的存储空间转换为连续存储空间的过程,Spark称之为“展开”(Unroll)。Block有序列化和非序列化两种存储格式,具体以哪种方式取决于该RDD的存储级别。非序列化的Block以一种DeserializedMemoryEntry的数据结构定义,用一个数组存储所有的Java对象实例,序列化的Block则以SerializedMemoryEntry的数据结构定义,用字节缓冲区(ByteBuffer)来存储二进制数据。每个Executor的Storage模块用一个链式Map结构(LinkedHashMap)来管理堆内和堆外存储内存中所有的Block对象的实例,对这个LinkedHashMap新增和删除间接记录了内存的申请和释放。

    因为不能保证存储空间可以一次容纳Iterator中的所有数据,当前的计算任务在Unroll时要向MemoryManager申请足够的Unroll空间来临时占位,空间不足则Unroll失败,空间足够时可以继续进行。对于序列化的Partition,其所需的Unroll空间可以直接累加计算,一次申请。而非序列化的Partition则要在遍历Record的过程中依次申请,即每读取一条Record,采样估算其所需的Unroll空间并进行申请,空间不足时可以中断,释放已占用的Unroll空间。如果最终Unroll成功,当前Partition所占用的Unroll空间被转换为正常的缓存RDD的存储空间,如下图2所示。

    图9 Spark Unroll示意图

    图8和图9中可以看到,在静态内存管理时,Spark在存储内存中专门划分了一块Unroll空间,其大小是固定的,统一内存管理时则没有对Unroll空间进行特别区分,当存储空间不足时会根据动态占用机制进行处理。

    淘汰与落盘

    由于同一个Executor的所有的计算任务共享有限的存储内存空间,当有新的Block需要缓存但是剩余空间不足且无法动态占用时,就要对LinkedHashMap中的旧Block进行淘汰(Eviction),而被淘汰的Block如果其存储级别中同时包含存储到磁盘的要求,则要对其进行落盘(Drop),否则直接删除该Block。
    存储内存的淘汰规则为:

    • 被淘汰的旧Block要与新Block的MemoryMode相同,即同属于堆外或堆内内存

    • 新旧Block不能属于同一个RDD,避免循环淘汰

    • 旧Block所属RDD不能处于被读状态,避免引发一致性问题

    • 遍历LinkedHashMap中Block,按照最近最少使用(LRU)的顺序淘汰,直到满足新Block所需的空间。其中LRU是LinkedHashMap的特性。

    落盘的流程则比较简单,如果其存储级别符合_useDisk为true的条件,再根据其_deserialized判断是否是非序列化的形式,若是则对其进行序列化,最后将数据存储到磁盘,在Storage模块中更新其信息。

    执行内存管理


    多任务间内存分配 

    Executor内运行的任务同样共享执行内存,Spark用一个HashMap结构保存了任务到内存耗费的映射。每个任务可占用的执行内存大小的范围为1/2N ~ 1/N,其中N为当前Executor内正在运行的任务的个数。每个任务在启动之时,要向MemoryManager请求申请最少为1/2N的执行内存,如果不能被满足要求则该任务被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。

    Shuffle的内存占用

    执行内存主要用来存储任务在执行Shuffle时占用的内存,Shuffle是按照一定规则对RDD数据重新分区的过程,我们来看Shuffle的Write和Read两阶段对执行内存的使用:

    • Shuffle Write

      • 若在map端选择普通的排序方式,会采用ExternalSorter进行外排,在内存中存储数据时主要占用堆内执行空间。

      • 若在map端选择Tungsten的排序方式,则采用ShuffleExternalSorter直接对以序列化形式存储的数据排序,在内存中存储数据时可以占用堆外或堆内执行空间,取决于用户是否开启了堆外内存以及堆外执行内存是否足够。

    • Shuffle Read

      • 在对reduce端的数据进行聚合时,要将数据交给Aggregator处理,在内存中存储数据时占用堆内执行空间。

      • 如果需要进行最终结果排序,则要将再次将数据交给ExternalSorter处理,占用堆内执行空间。

    在ExternalSorter和Aggregator中,Spark会使用一种叫AppendOnlyMap的哈希表在堆内执行内存中存储数据,但在Shuffle过程中所有数据并不能都保存到该哈希表中,当这个哈希表占用的内存会进行周期性地采样估算,当其大到一定程度,无法再从MemoryManager申请到新的执行内存时,Spark就会将其全部内容存储到磁盘文件中,这个过程被称为溢存(Spill),溢存到磁盘的文件最后会被归并(Merge)。

    Shuffle Write阶段中用到的Tungsten是Databricks公司提出的对Spark优化内存和CPU使用的计划,解决了一些JVM在性能上的限制和弊端。Spark会根据Shuffle的情况来自动选择是否采用Tungsten排序。Tungsten采用的页式内存管理机制建立在MemoryManager之上,即Tungsten对执行内存的使用进行了一步的抽象,这样在Shuffle过程中无需关心数据具体存储在堆内还是堆外。每个内存页用一个MemoryBlock来定义,并用Object obj和long offset这两个变量统一标识一个内存页在系统内存中的地址。堆内的MemoryBlock是以long型数组的形式分配的内存,其obj的值为是这个数组的对象引用,offset是long型数组的在JVM中的初始偏移地址,两者配合使用可以定位这个数组在堆内的绝对地址;堆外的MemoryBlock是直接申请到的内存块,其obj为null,offset是这个内存块在系统内存中的64位绝对地址。Spark用MemoryBlock巧妙地将堆内和堆外内存页统一抽象封装,并用页表(pageTable)管理每个Task申请到的内存页。

    Tungsten页式管理下的所有内存用64位的逻辑地址表示,由页号和页内偏移量组成:

    1. 页号:占13位,唯一标识一个内存页,Spark在申请内存页之前要先申请空闲页号。
    2. 页内偏移量:占51位,是在使用内存页存储数据时,数据在页内的偏移地址。
    

    有了统一的寻址方式,Spark可以用64位逻辑地址的指针定位到堆内或堆外的内存,整个Shuffle Write排序的过程只需要对指针进行排序,并且无需反序列化,整个过程非常高效,对于内存访问效率和CPU使用效率带来了明显的提升。

    小结



    Spark的存储内存和执行内存有着截然不同的管理方式:对于存储内存来说,Spark用一个LinkedHashMap来集中管理所有的Block,Block由需要缓存的RDD的Partition转化而成;而对于执行内存,Spark用AppendOnlyMap来存储Shuffle过程中的数据,在Tungsten排序中甚至抽象成为页式内存管理,开辟了全新的JVM内存管理机制。

    展开全文
  • spark堆内和堆外内存

    2021-05-17 13:30:12
    Spark对堆内内存进行JVM内存管理,引入了堆外内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用;其中,堆外内存直接向操作系统申请。 二、堆内内存(On-Heap Memory) ​ 堆内内存概述:

    由于Driver的内存管理较为简单,内存管理主要对Executor的内存管理进行探讨。

    一、堆内(On-Heap Memory)和堆外(Off-Heap Memory)内存规划

    ​ Executor作为一个JVM进程,Executor的内存管理建立在JVM的内存管理之上。Spark对堆内内存进行JVM内存管理,引入了堆外内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用;其中,堆外内存直接向操作系统申请。
    在这里插入图片描述

    二、堆内内存(On-Heap Memory)

    ​ 堆内内存概述:

    在这里插入图片描述

    ​ 在Spark程序启动时,堆内内存的大小由spark-submit中的–executor-memory 或 spark.executor.memory参数配置。Spark对于堆内内存的管理是一种逻辑上的“规划式”管理,因为对象实例占用内存的申请和释放都由JVM完成,Spark只能在申请后和释放前记录这些内存。
    对于Spark的序列化对象,由于是字节流的形式,其占用的内存大小可以直接计算,而对于非序列化对象,其占用的内存是通过周期性的采样估算而得,即并不是新增数据项都会计算一次占用内存的大小,这种方法降低了时间开销,但是有可能误差较大,导致某一时刻的实际内存远远超出预期。所以Spark并不能准确记录实际可用的堆内内存,从而也无法避免内存溢出—OOM。

    1.内存空间的动态分配

    ​ 在Spark1.6之前使用静态内存管理,即存储内存、执行内存和其他内存的大小在Spark应用程序运行期间是固定的。缺点很多,因此在Spark1.6以后,堆内内存和堆外内存的管理采用动态分配的形式,即:存储内存(Storage)和执行内存(Execution)共享同一块存储空间,双方可以动态的占用对方的空闲区域。

    ​动态占用机制的规则如下:

    1.首先设定基本的Storage和Execution的区域(spark.storage.storageFraction参数),该设定确定了双方各自拥有的空间范围;

    2.双方的空间都不足时,则都存储到磁盘上面;

    3.若己方空间不足,而对方空间空余时,可借用对方的空间(存储空间不足指不足以放下一个完整的Block)。

    在这里插入图片描述
    在这里插入图片描述

    ​ 在上图1中的情况中,Storage占用了Execution的空余内存后,Execution要开始用了,想把虚线标示的内存要回来,于是Storage便开始在自己缓存的RDD中,找到 StorageLevel.MEMORY_AND_DISK / .MEMORY_AND_DISK_2 / .MEMORY_AND_DISK_SER / .MEMORY_AND_DISK_SER_2 的数据,溢写到磁盘上。如果写到磁盘上以后,还是达不到虚线的标准,在这一部分存储的数据,会直接被删除。鉴于此种状况,cache / persist一般都与checkpoint结合使用。Spark-core的checkpoint将一个RDD存储到分布式文件系统中;Spark-streaming的checkpoint:1.保存整个运行环境;2.保存未处理的RDD,便于恢复故障;

        在上图2中的情况中,Execution占用了Storage的空余内存,但是Storage要开始用了,想把虚线标示的内存要回来,这时候!Execution对Storage理直气壮地说:我的Shuffle调度时对内存的应用太复杂了,给你腾不出来地方,这些内存就不还给你了!Storage当然义愤填膺的说:好!
    

    2.淘汰与落盘

        由于同一个Executor的所有计算任务共享有限的存储内存空间,当有新的Block需要缓存但是剩余的内存空间不足且无法动态占用时,就要对LinkHashMap中的旧Block进行淘汰(Eviction),而被淘汰的Block如果存储级别中包含存储到磁盘的要求,则要对其进行落盘(Drop),否则直接删除该Block。遍历LinkedHashMap中Block,按照LRU进行淘汰,被淘汰的旧Block与新Block的MemoryMode相同,即同属堆外或堆内内存;新旧Block不能同属一个 RDD,避免循环淘汰。
    

    ​ 在RDD的缓存时,将原先存在Others中的含有多个Partition,且每个Partition中的record不连续的RDD,转存至Storage中,并且每个RDD中含有多个Block,且每个Block中的record连续,这个过程称为Unroll。

    ​ Storage模块在逻辑上以Block为基本存储单位,RDD的每个Partition经过处理后唯一对应一个Block(BlockId格式为rdd_RDD-ID_PARTITION-ID)。Driver端的Master负责整个Spark应用程序的Block的元数据信息管理和维护,而Executor端的Slave负责将Block的更新状态上报到Master,同时接收Master的命令,例如新增或者删除一个RDD。

    3.执行内存(Execution)

    ​ Execution主要用来存储任务在执行Shuffle时占用的内存,Shuffle是按照一定规则对RDD数据重新分区的过程,由Shuffle Write和Shuffle Read两个阶段组成。

    3-1 Shuffle Write

    ​ (1) 若在map端选用普通的排序方式,会采用ExternalSorter进行外排,在内存中存储数据时主要占用堆内执行空间;

    ​ (2)若在map端选择Tungsten方式,则采用ShuffleExternalSorter直接对序列化形式存储的数据排序,在内存中执行存储数据时可以使用堆内或堆外执行空间,取决了用户是否开启了堆外内存及堆外内存是否足够。

    3-2 Shuffle Read

    ​ (1)在对reduce端数据进行聚合时,要将数据交给Aggregator处理,在内存中存储数据时占用堆内执行空间;

    ​ (2) 若对最终结果进行排序,则再次将数据交给ExternalSorter处理,此时占用堆内执行空间;

    ​ 在ExternalSorter和Aggregator中,Spark会使用一种叫AppendOnlyMap的哈希表在堆内执行内存中存储数据,但是在Shuffle过程中并不是所有的数据都能保存到哈希表中,这个哈希表占用的内存会周期性的进行估算,当达到一定程度,无法再从MemoryManager中申请到执行内存时,Spark就会全部存储到磁盘文件中,这个过程被称为溢存(Spill),溢存到磁盘的文件最后会被归并(Merge)。

    4.其他内存(other)

    ​ 这里用于存储没有被缓存的RDD、元数据及Spark对象实例。其中,RDD以Partition为基本存储单位。

    总结

    ​ Spark的存储内存和执行内存有着截然不同的管理方式:对于Storage来说,Spark用一个LinkedHashMap来集中管理所有的Block,Block由需要缓存的RDD的Partition转化而成,而对于Execution来说,Spark用AppendOnlyMap来存储Shuffle过程中的数据,在Tungsten排序中甚至抽象成为页式管理,开辟了全新的JVM内存管理机制。

    三、堆外内存(Off-Heap Memory)

    ​ Spark引入堆外内存(Off-Heap),使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据;

    ​ 堆外内存意味着把内存对象分配到Java虚拟以外的内存,这些内存直接受操作系统(而不是虚拟机)管理。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。Spark可以直接操作系统堆外内存,减少了不必要的系统开销,以及频繁的GC扫描和回收,提高了处理性能。堆外内存可以被精确地申请和释放(JVM对于内存的清理是无法精确指定时间点的,因此无法实现精确的释放),而且序列化的数据占用的空间可以被精确地计算,所以相比堆内内存来说降低了难度,也降低了误差;

    ​ 在默认情况下堆外内存并不启用,可以通过配置spark.memory.offheap.enabled参数启用,并由spark.memory.offheap.size设定堆外空间的大小。除了没有Other空间外,堆内内存与堆外内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。
    在这里插入图片描述

    四、spark常用参数说明

    1.num-executors

    参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。
    参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

    2.executor-memory

    参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。
    参数调优建议:每个Executor进程的内存设置4G8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/31/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

    3.executor-cores

    参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。
    参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

    4.driver-memory

    参数说明:该参数用于设置Driver进程的内存。
    参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

    5.spark.default.parallelism

    参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。
    参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

    6.spark.storage.memoryFraction

    参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。
    参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

    7.spark.shuffle.memoryFraction

    参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。
    参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

    8.total-executor-cores

    参数说明:Total cores for all executors.

    9.资源参数参考示例

    以下是一份spark-submit命令的示例:

    ./bin/spark-submit \
      --master spark://192.168.1.1:7077 \
      --num-executors 100 \
      --executor-memory 6G \
      --executor-cores 4 \
     --total-executor-cores 400 \ ##standalone default all cores 
      --driver-memory 1G \
      --conf spark.default.parallelism=1000 \
      --conf spark.storage.memoryFraction=0.5 \
      --conf spark.shuffle.memoryFraction=0.3 \
    
    展开全文
  • 一、内存组成通常JVM的参数我们会配置-Xms 初始内存-Xmx 最大内存-XX:+UseG1GC/CMS 垃圾回收器-XX:+DisableExplicitGC 禁止显示GC-XX:MaxDirectMemorySize 设置最大内存,默认是-xmx-survivor,也就是...
  • 1. 引言很久没有遇到内存相关的问题了,五一假期刚结束,便不期而遇,以前也处理过几次这类问题,但都没有总结,觉得是时候总结一下了。先来看一个 Demo:在 Demo 中分配内存用的是 allocateDirect 方法,但...
  • 1. 引言很久没有遇到内存相关的问题了,五一假期刚结束,便不期而遇,以前也处理过几次这类问题,但都没有总结,觉得是时候总结一下了。先来看一个 Demo:在 Demo 中分配内存用的是 allocateDirect 方法,但...
  • 堆内存,最大的内存区域 如题,我觉得这样说一点都不为过。 给大家看一张生产环境的堆内存和非堆内存的监控图片。 大家观察下图中Heap Usage 设置的 最大内存,已经超过4GB,那么我们对比一下右边的non-Heap ...
  • 堆栈内存指的是堆内存和栈内存堆内存是GC管理的内存,栈内存是线程内存堆内存结构: 还有一个更细致的结构图(包括MetaSpace还有code cache):注意在Java8以后PermGen被MetaSpace代替,运行时可自动扩容,...
  • 最近一个项目总发出堆内存过大报警,dump出了内存也一直没时间研究,今天正好整理下相关用到的命令和代码这里有几个主要的命令和工具jinfo:可以输出并...可以用来监视VM内存内的各种和非的大小及其内存使用量...
  • 一、JDK1.8 JVM运行时数据区域概览这里介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久...
  • 1、堆外内存定义内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用...这样做的结果就是能保持一个较小的堆内内存,以减少垃...
  • 最近经常有人问我在Java中使用外(off heap)内存的好处与用途何在。我想其他面临几样选择的人应该也会对这个答案感兴趣吧。 内存其实并无特别之处。线程栈,应用程序代码,NIO缓存用的都是内存。事实上...
  • Java 内存

    2021-02-27 23:06:45
    堆外内存的创建 在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限, 堆外内存的限额默认与堆内内存(由-Xmx ...
  • 详解Java内存

    2021-03-05 15:45:02
    临近春节,最近有点时间,准备顺着上篇专栏的思路写下去,...java虚拟机规范把这些内存区域叫做运行时数据区:而内存,是指分配在java外的内存区域,其不受jvm管理,不会影响gc。本文将以java.nio.DirectByt...
  • 和非堆内存按照官方的说法:“Java 虚拟机具有一个(Heap),是运行时数据区域,所有类实例和数组的内存均从此处分配。是在 Java 虚拟机启动时创建的。”“在JVM中之外的内存称为非堆内存(Non-heap memory)...
  • 但有利必有弊,堆内内存主要有两个缺点:1.GC是有成本的,堆中的对象数量越多,GC的开销也会越大。2.使用堆内内存进行文件、网络的IO时,JVM会使用堆外内存做一次额外的中转,也就是会多一次内存拷贝。和堆内内存相...
  • NIO 内存

    2021-02-26 12:36:01
    Java 内存模型内存 / 直接内存(Direct Memory)JDK1.4 中引入的NIO类,基于 channel 和 Buffer 的 I/O 方式,可用 Native 库直接分配内存,然后利用一个存储在中的 DirectByteBuffer 对象作为这块内存引用...
  • Java 内存的使用

    2021-03-16 01:55:49
    更多 Java 虚拟机方面的文章,请参见文集《Java 虚拟机》为什么需要使用内存将长期存活的对象(如 Local Cache )移入内存( off-heap,又名直接内存 direct-memory),从而减少 CMS 管理的对象数量, 以降低 ...
  • 内存分析文章涉及JVM的垃圾回收,主要讲的是通过使用「内存」对Young GC进行优化。文章中介绍,MsgBroker消息中间件会对消息进行缓存,JVM需要为被缓存的消息分配内存,首先会被分配到年轻代。当缓存中的消息...
  • Java内存的使用

    2021-03-01 08:15:39
    Java内存的使用Published:26 Dec 2014Category:最近经常听到有人问Java中使用外(off heap)内存的好处与用途何在。我想其他面临几样选择的人应该也会对这个答案感兴趣吧。内存其实并无特别之处。线程栈,...
  • JDK5 之后才出现了堆外内存得API给到开发进行调用,那么我们为什么要使用堆外内存呢? 加速GC回收,大量对象产生在堆内,GC回收得压力是很大得 ... 利用NIO得ByteBuffer,JVM会进行堆外内存管理,当堆内得引.
  • 微信公众号: Java随笔录 关注可了解更多Java相关的技术分享。问题或建议,欢迎公众号留言! 目录Java堆栈内存内存1....堆栈内存,顾名思义,指的是堆内存以及栈内存,其中,堆内存是由Java GC进行管理的.
  • 但自从用了Netty,就变成了天天打交道的事情,毕竟内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。好在,Netty所用的内存只是Java NIO的 DirectByteBuffer类,...
  • JVM内存如何回收

    2021-02-23 15:37:19
    文章目录JVM内存如何回收什么是内存堆内存的使用JVM如何回收内存 什么是内存 内存(直接内存)不是JVM运行时数据区的一部分,也不是虚拟机规范中定义的内存区域 直接内存是在Java外,直接...
  • 一、内存组成通常JVM的参数我们会配置-Xms 初始内存-Xmx 最大内存-XX:+UseG1GC/CMS 垃圾回收器-XX:+DisableExplicitGC 禁止显示GC-XX:MaxDirectMemorySize 设置最大内存,默认是-xmx-survivor,也就是...
  • 最近排查一个线上java服务常驻内存异常高的问题,大概现象是:javaXmx配置了8G,但运行一段时间后常驻内存RES从5G逐渐增长到13G #补图#,导致机器开始swap从而服务整体变慢。由于Xmx只配置了8G但RES常驻内存达到了...
  • 在我的上一篇文章别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】中,相信大家已经对java类加载机制有一个比较全面的理解了,那么类加载之后,字节码数据在 Java 虚拟机内存中是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 475,638
精华内容 190,255
关键字:

堆内内存