直接内存

Java、操作系统都可以共同访问的内存区域。不属于JVM内存区域,直接内存可以提高文件IO性能。

为什么会有直接内存呢?

先了解一下操作系统与我们JavaIO操作时CPU用户态、内核态切换:https://www.zanglikun.com/13945.html

我们常规IO是BIO是会阻塞的,比如我内存中有一个文件,我们用byte[]来读取内存文件写入硬盘的,此时我们获取byte[]是在用户态进行的,调用操作系统的写的时候,就会切换到内核态。内核态完成操作后,就会切换会用户态。依此循环,直接文件写完。这样看来,性能非常差了。频繁的用户态->内核态切换,缺失影响性能,这也就是NIO存在的原因。

直接内存也会OOM

我们知道直接内存不在JVM虚拟机中,但是系统总内存有限,系统内存不够用了。肯定也会out.of.memory!

直接内存不会收到JVM的垃圾回收机制。

我们通常会主动去释放这个空间。底层原理是对象的虚引用(Cleaner)。当对象是虚引用(Cleaner)被GC的时候,就会触发虚引用的线程,底层调用了unsafe.freeMemory()实现对直接内存回收。几乎所有的框架都是使用Unsafe操作直接内存的!

NIO包下开辟直接内存

    @Test
    void sleep() {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
        try {
            Thread.sleep(15000);
            byteBuffer = null;
            System.gc(); // Full GC
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

Unsafe开辟直接内存

    @SneakyThrows
    @Test
    void sleep() {
        try {
            Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) unsafeField.get(null);
            // 分配了单没占用(看不到物理内存被占用):分配以字节为单位的给定大小的新的本机内存块。内存内容未初始化;它们通常都是垃圾
            long memoryAddress = unsafe.allocateMemory(1024 * 1024 * 1024); //1GB
            // 现在将分配的内存使用了:将给定内存块中的所有字节设置为固定值(通常为零)
            unsafe.setMemory(memoryAddress, 1024 * 1024 * 1024, (byte) 0);
            Thread.sleep(15000);
            unsafe.freeMemory(memoryAddress); // 主动释放内存
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

我们NIO底层就是使用Unsafe。去释放直接内存的。

禁用程序员写的System.gc() & -XX:+DisableExplicitGC

这里禁用只是代码中的System.gc()。JVM自带的垃圾回收会处理的

添加JVM参数-XX:+DisableExplicitGC

JVM 垃圾回收

1、如何判断对象可以被回收

  • 引用计数法
    • 通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
  • 可达性分析
    • 可达性分析:又称为根搜索算法、追踪性垃圾收集
    • GC Root引用的对象不会被回收,如果堆内存中没有呗Root对象引用,就会被垃圾回收
  • 四种引用
    • 强引用:我们常见的引用就是强引用:只要对象没有被置null,在GC时就不会被回收。
    • 软引用:只有在内存不足的情况下,被引用的对象才会被回收。SoftReference<String> soft = new SoftReference<>(String); 那么这个soft.get()对象就是软引用。
    • 弱引用:只要GC,就会被垃圾回收。使用方式 WeakReference<String> weak = new WeakReference<>(String); 通过weak.get()获取这个对象
    • 虚引用:如果一个对象只有虚引用,就相当于没有引用,在任何时候都可能会被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。

软引用示例

    @SneakyThrows
    public static void main(String[] ars) {
        // 声明一个引用队列,本身软引用对象也是占用内存的,如果软引用引用的对象被回收了,我们肯定希望软引用对象也被回收,就放入引用队列自动回收
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue();
        // list 集合存放的搜时软引用对象
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> obj = new SoftReference<>(new byte[5 * 1024 * 1024], referenceQueue);
            list.add(obj);
            System.out.println("添加成功第:" + list.size() + "次,对象是" + obj.get());
        }
        System.out.println("List集合添加完毕,当前List集合大小是:" + list.size());
        System.out.println("开始回收引用对象");
        Reference<? extends byte[]> poll = referenceQueue.poll();
        int count = 1;
        while (poll != null) {
            System.out.println("删除第"+(count++)+"次引用对象:" + poll);
            // 从集合删除此软引用
            list.remove(poll);
            // 从队列删除软引用
            poll = referenceQueue.poll();
        }
        System.out.println("引用对象删除后,当前List集合大小是:" + list.size());
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i).get());
        }
    }

添加JVM参数-Xmx10m后,运行输出

添加成功第:1次,对象是[B@dcf3e99
添加成功第:2次,对象是[B@6d9c638
添加成功第:3次,对象是[B@7dc5e7b4
添加成功第:4次,对象是[B@1ee0005
添加成功第:5次,对象是[B@75a1cd57
List集合添加完毕,当前List集合大小是:5
开始回收引用对象
删除第1次引用对象:java.lang.ref.SoftReference@3d012ddd
删除第2次引用对象:java.lang.ref.SoftReference@6f2b958e
删除第3次引用对象:java.lang.ref.SoftReference@1eb44e46
删除第4次引用对象:java.lang.ref.SoftReference@6504e3b2
引用对象删除后,当前List集合大小是:1
[B@75a1cd57

弱引用示例

    @SneakyThrows
    public static void main(String[] ars) {
        // 声明一个引用队列,本身软引用对象也是占用内存的,如果软引用引用的对象被回收了,我们肯定希望软引用对象也被回收,就放入引用队列自动回收
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue();
        // list 集合存放的搜时软引用对象
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            WeakReference<byte[]> obj = new WeakReference<>(new byte[5 * 1024 * 1024], referenceQueue);
            list.add(obj);
            System.out.println("添加成功第:" + list.size() + "次,对象是" + obj.get());
        }
        System.out.println("List集合添加完毕,当前List集合大小是:" + list.size());
        System.out.println("程序员强制执行Full GC");
        System.gc();
        System.out.println("执行GC后开始回收引用对象");
        Reference<? extends byte[]> poll = referenceQueue.poll();
        int count = 1;
        while (poll != null) {
            System.out.println("删除第"+(count++)+"次引用对象:" + poll);
            // 从集合删除此软引用
            list.remove(poll);
            // 从队列删除软引用
            poll = referenceQueue.poll();
        }
        System.out.println("引用对象删除后,当前List集合大小是:" + list.size());
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i).get());
        }
        Thread.sleep(30000);
        System.out.println("JVM睡眠后list集合长度是:"+list.size());
    }

添加JVM参数-Xmx10m后,运行输出

添加成功第:1次,对象是[B@dcf3e99
添加成功第:2次,对象是[B@6d9c638
添加成功第:3次,对象是[B@7dc5e7b4
添加成功第:4次,对象是[B@1ee0005
添加成功第:5次,对象是[B@75a1cd57
List集合添加完毕,当前List集合大小是:5
程序员强制执行Full GC
执行GC后开始回收引用对象
删除第1次引用对象:java.lang.ref.WeakReference@3d012ddd
删除第2次引用对象:java.lang.ref.WeakReference@6f2b958e
删除第3次引用对象:java.lang.ref.WeakReference@1eb44e46
删除第4次引用对象:java.lang.ref.WeakReference@6504e3b2
删除第5次引用对象:java.lang.ref.WeakReference@515f550a
引用对象删除后,当前List集合大小是:0
JVM睡眠后list集合长度是:0

2、垃圾回收算法

1、标记清除

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

标记清除算法的主要缺点有个:

(1)空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。

2、标记整理

标记整理算法,是对标记清除算法的优化,它将内存不连续的区域移动到连续空间,供给其他线程使用。

标记整理算法的缺点是:大量利用复制,导致性能差一些

3、复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

   它的主要缺点有两个:
        (1)效率问题:在对象存活率较高时,复制操作次数多,效率降低;
        (2)空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)

3、分代垃圾回收

本质是将内存划分不同区域,用不同的垃圾算法回收内存

JVM将堆内存分为新生代、老年代

新生代分为:伊甸园、幸存区

整体就可以堪称伊甸园、幸存区From、幸存区To。内存比例是8:1:1

8:1:1来源

这个比例是:有研究认为98%的对象都是朝生夕死,也就是说每次MinorGC基本也就是使用2%的内存空间,但考虑到实验偏差以及实际情况的多样性,jvm默认预留了10%的内存用于存放存活对象。也就是老年代,90%给了新生代,于是根据新生代分配就变成伊甸园、幸存区From、幸存区To比例是8:1:1

新生代与老年代比例

默认是新生代:老年代 = 1:2。

可以通过-XX:NewRatio=ratio调整其比值就是ratio 就是新生代:老年代=1:ratio

个人觉得,如果有大文件需求,还是不要去修改新生代、老年代比例。毕竟如果单个内存占用过大,就会直接进入老年代中。避免频繁的MirorGC。如果设计大量的小文件,那就给调整-XX:NewRatio=1。让新生代大一些,减少MirorGC的次数。

-XX:NewSize选项等效于-Xmn。标识指定新生代初始大小,Oracle 建议您将年轻代的大小保持在总堆大小的一半到四分之一之间

新生代

伊甸园eden、幸存区From、幸存区To,其整体比例就变成8:1:1

新生代垃圾回收流程

对象new会放在伊甸园。如果伊甸园内存不够就会触发(新神代GC)MinorGC。开始执行标记复制的算法:存活的对象(伊甸园、幸存区From都有)使用Copy垃圾回收算法复制到幸存区To,此时改对象的对象头(4bit 最大就是15) 幸存标记+1。当对象复制完毕,就会触发幸存区From->幸存区To的转换。然后 幸存区From直接清除。

如果幸存标记(默认)达到15次,那就标记到老年代中。

老年代(回收算法采用:标记清除或标记整理)

一个对象幸存次数达到15次,就会被移动至老年代。当老年代内存空间不足时,就会触发FullGC。如果Full GC执行完毕后,内存依旧不足,直接OOM

Minor GC与Full GC会引发stop the world

也就是说:暂停所有JVM线程,只保留垃圾回收线程。

注意:如果一个大对象大于新生代内存,会尝试直接放入老年代。

案例分析

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // 堆大小20m,新生代10m,指定Serial垃圾回收器,打印GC详情
    // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {

    }

控制台输出:

Heap
 def new generation   total 9216K, used 6358K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000) 这行是:新生代内存概述
  eden space 8192K,  77% used [0x00000007bec00000, 0x00000007bf235820, 0x00000007bf400000) 这行是:eden区内存使用情况
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000) 这行是:幸存区From 内存使用情况
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000) 这行是:幸存区To 内存使用情况
 tenured generation   total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000) 这行是:老年代内存概述
   the space 10240K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
 Metaspace       used 3202K, capacity 4564K, committed 4864K, reserved 1056768K 这行是:元空间内存概述
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

元空间不属于堆的内存,只要存储类的class文件。

当前Main方法空运行 可见eden区直接占用77%,我们直接测试,添加8M的内存,看看内存占用在那里?

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // 堆大小20m,新生代10m,指定Serial垃圾回收器,打印GC详情
    // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
        byte[] bytes = new byte[_8MB];
    }
Heap
 def new generation   total 9216K, used 6521K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  79% used [0x00000007bec00000, 0x00000007bf25e7c8, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 8192K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  80% used [0x00000007bf600000, 0x00000007bfe00010, 0x00000007bfe00200, 0x00000007c0000000)
 Metaspace       used 3408K, capacity 4564K, committed 4864K, reserved 1056768K
  class space    used 371K, capacity 388K, committed 512K, reserved 1048576K

直接看老年代,发现我们的对象直接进入老年代,没有触发任何GC。也就是对象直接进入老年代了。

4、垃圾回收器

可以通过:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 可以通过Oracle官网查看具体含义 直接Ctrl + F搜索即可。

1、串行垃圾回收器(我们不用)

特点:单线程、堆内存较小、适合个人电脑(单核CPU以为就一个线程,最多一个CPU核心就够了)

注意 Serial是新生代垃圾回收,采用是复制算法、SerialOld是老年代垃圾回收采用的标记整理

-XX:+UseSerialGC 启用串行垃圾收集器的使用。对于不需要垃圾收集的任何特殊功能的小型简单应用程序,这通常是最佳选择。默认情况下,禁用此选项,并根据机器的配置和 JVM 的类型自动选择收集器。

2、吞吐量优先垃圾回收器

特点:多线程、适合堆内存较大,CPU核心数多一些。让单位时间内,STW时间最短。白话就是 我一小时1次。

-XX:+UseParallelGC 允许使用并行清除垃圾收集器(也称为吞吐量收集器),通过利用多个处理器来提高应用程序的性能。启用此,则该-XX:+UseParallelOldGC选项将自动启用

CPU占用是不均匀的,当执行GC时,所有CPU去搞垃圾回收。

3、响应时间优先垃圾回收器 CMS

多线程、适合堆内存较大,CPU核心数多一些。尽量控制单次STW时间短一些。白话就是 我一小时10次。

-XX:+UseConcMarkSweepGC 为老年代启用 CMS 垃圾收集器 启用此选项后,该-XX:+UseParNewGC选项会自动设置

收集过程:CMS收集器是基于算法标记-清除来实现的,整个过程分为5步:

初始标记
记录能被GC Root直接引用的对象,触发一次STW,但是这次STW很快,因为在标记的过程中不会标记一整条引用链的对象,如图所示,只记录红色箭头关联到的对象,不记录黑色箭头。

并发标记
从GC Roots的直接引用对象开始依次扫描(对上面的黑色箭头的链路做扫描),这个过程需要比较多的时间,用户线程和GC线程同时执行,不会产生STW,因为在扫描的过程中用户线程还在不断的执行所以可能会出现标记过的对象又变成了垃圾。

重新标记
重新标记需要Stop The World,这个阶段是为了修正在并发标记阶段产生的浮动垃圾,对标记过的对象进行。

并发清除
GC线程和用户线程同时进行,开始清除未被标记的垃圾,在此阶段也会产生垃圾(浮动垃圾),产生垃圾后无法清除,只能留待下一次GC。

本质是多线程并行执行。并行就是比如4个线程中,1个线程去处理垃圾。3个线程是会用户线程。只有某些时间点是STW,其余时间用户线程是正常运行的。

CMS算法可能会造成并发失败问题,就会变成串行垃圾回收,当串行成功后,在变为并发垃圾回收了。此时,就会造成时间变得特别长了。

4、G1 垃圾回收 JDK9变为默认垃圾回收了

-XX:+UseG1GC 开启它是一种服务器式垃圾收集器,针对具有大量 RAM 的多处理器机器。它以高概率满足 GC 暂停时间目标,同时保持良好的吞吐量。建议将 G1 收集器用于需要大堆(大小约为 6 GB 或更大)且 GC 延迟要求有限(稳定且可预测的暂停时间低于 0.5 秒)的应用程序。

将堆内存话费多个大小相等的Region区域

整体上是标记 + 整理算法,多个Region区域是复制算法

-XX:MaxGCPauseMillis=500 设置最大 GC 暂停时间(以毫秒为单位)的目标。这是一个软目标,JVM 将尽最大努力实现它。默认情况下,没有最大暂停时间值。
G1垃圾回收阶段
新生代回收-> 新生代回收+并发标记 ->混合收集(新生代、老年代)

5、垃圾回收调优

特殊说明:
上述文章均是作者实际操作后产出。烦请各位,请勿直接盗用!转载记得标注原文链接:www.zanglikun.com
第三方平台不会及时更新本文最新内容。如果发现本文资料不全,可访问本人的Java博客搜索:标题关键字。以获取最新全部资料 ❤