JVM系列——直接内存,垃圾回收day1-3

181 篇文章 3 订阅
7 篇文章 0 订阅

直接内存(direct memory)

指的是操作系统内存
常见于NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受JVM内存回收管理

主要作用

进行大文件的读写,效率很高

不用直接内存:Java不具备磁盘读写的能力,当我们要进行磁盘读写时需要调用本地方法接口,从用户态转换到内核态,此时磁盘文件被内存读取进入系统内存的系统缓存区,再经由系统缓存区复制进入Java堆内存的Java缓冲区(以这种方式效率是不高的!)

当我们使用直接内存时:磁盘文件会进入系统内存和Java堆内存公用的直接内存区,无需进行复制操作,速度成倍提升

如果大家想要好好了解这个方面可以查看我的IO系列文章

直接内存溢出(Direct buffer memory)

我们可以直接查看计算机的内存来观测直接内存

直接内存的分配

Java中使用底层实现类Unsafe
Unsafe是用反射的方式获取的,无法new

//获取unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe us = (Unsafe)field.get(null);
//分配空间
long baseMemory = us.allocateMemory(_2Gb);
us.setMemory(baseMemory,_2Gb,(byte)0)

直接内存的释放

使用freeMemory方法
首先要明确一点:垃圾回收机制不能释放直接内存
直接内存的释放需要我们主动调用方法

us.freeMemory(baseMemory)
使用ByteBuffer的场景

ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory 来释放直接内存

禁用显式垃圾回收

使用-XX:+DisableExplicitGC

可以直接无效化System.gc()进行显式垃圾回收,因为显示垃圾回收会同时回收新生代和老年代,效率低,影响性能

垃圾回收GC

如何判断对象可以被回收

引用计数法

当一个对象被其他变量所引用时,就让这个对象的引用计数+1,当没有引用时,计数为0,进行垃圾回收

问题

若使用这种方法会出现循环引用的问题,即两个对象互相引用导致无法将计数清0,垃圾回收不触发

可达性分析法

需要确定根对象(即不能当成垃圾来被垃圾回收的对象),垃圾回收前,会对堆中的对象进行扫描,若有对象被根对象直接或间接引用则不能被回收,否则可以回收

哪些对象可以作为GC ROOT

我们可以采用Memory Analyzer(MAT)工具对堆进行分析,有助于找出堆内存泄漏问题

下载

https://www.eclipse.org/downloads/download.php?file=/mat/1.13.0/rcp/MemoryAnalyzer-1.13.0.20220615-win32.win32.x86_64.zip
你可以点链接进入下载,或者复制地址用下载器或者程序代理下载
在这里插入图片描述

使用
import java.io.IOException;
import java.util.ArrayList;

public class GCMemory {
    public static void main(String[] args) throws IOException {
        ArrayList<Object> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add(5);
        System.out.println("over 1");
        System.in.read();
        
        list = null;
        System.out.println("over 2");
        System.in.read();
        System.out.println("over all");
    }
}

1.使用jps查看进程号

jps

在这里插入图片描述

2.使用jmap获取内存快照中的活值(二进制格式)
这样这条快照信息会保存在当前目录下的1.bin文件中

jmap -dump:format=b,live,file=1.bin 进程号

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

3.打开Memory Analyzer
在这里插入图片描述
在这里插入图片描述
4.选择快照文件

在这里插入图片描述
在这里插入图片描述
5.选择GC Root
在这里插入图片描述
在这里插入图片描述

五种引用

软引用

某个对象未被GC Root强引用所引用,在内存不足时垃圾回收会将其回收掉,当使用到引用队列时,回收后进入引用队列中(引用队列可以做到彻底释放软、弱引用的空间)

new SoftReference<>()

弱引用

和软引用差不多,但是不同在于只要进行垃圾回收就会回收掉,不用看内存足不足,当使用到引用队列时,回收后进入引用队列中

new WeakReference<>()

强引用

无法被垃圾回收的,直接被GC Root引用的就是强引用

虚引用

必须配合引用队列进行使用,在进行垃圾回收时进入引用队列,线程间接调用Unsafe.freeMemory释放直接内存

new PhantomReference<>()

终结器引用

必须配合引用队列进行使用,当一个对象重写了finallize()方法,并无强引用进行引用时,虚拟机会帮助创建一个终结器引用,当垃圾回收时进入引用队列,当finallizeHandlerThread线程查看引用队列中是否含有终结器引用,若有则调用对象的finallize方法进行回收

终结器引用的效率较低,优先级低,可能长时间无法释放,故不推荐

new FinalReference<>()

使用引用队列

使用引用队列清除软引用

new ReferenceQueue<>()
//关联引用队列
//传递参数
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
//关联
SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024*1024*3],referenceQueue);

完整示例

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;

public class SoftRef {
    public static void main(String[] args) {
        ArrayList<SoftReference<byte[]>> softReferenceArrayList = new ArrayList<>();
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        //关联
        SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024*1024*3],referenceQueue);
        softReferenceArrayList.add(softReference);
        //获取引用队列中的软引用
        Reference<? extends byte[]> poll = referenceQueue.poll();
        //移除软引用
        while (poll!=null){
            softReferenceArrayList.remove(poll);
            poll = referenceQueue.poll();
        }
    }
}

使用引用队列清除软引用

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;

public class WeakRef {
    public static void main(String[] args) {
        ArrayList<WeakReference<byte[]>> weakReferenceArrayList = new ArrayList<>();
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<byte[]>();
        WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024*1024],referenceQueue);
        weakReferenceArrayList.add(weakReference);
        Reference<? extends byte[]> poll = referenceQueue.poll();
        //移除
        while (poll!=null){
            weakReferenceArrayList.remove(poll);
            poll = referenceQueue.poll();
        }
    }
}

垃圾回收算法

1. 标记清除算法(mark sweep)

首先标记出未被GC Root所强引用的对象,然后对标记出的对象所释放即可,这里的清除指的是将标记的对象的地址放入一个空闲的地址列表中,下次进行分配新对象时就会进入这个空闲列表中获取空闲的空间进行内存分配

优点
  • 速度快
  • 简单
缺点
  • 易产生内存碎片

当我们要分配一个占用内存较大的对象时,导致无法插入原先的空间中,因为原始空间的不连续所导致的,这样导致了新的对象无法正常分配内存空间,以至于内存溢出

2. 标记整理算法(mark compact)

首先标记出未被GC Root所强引用的对象,然后在释放时进行整理
这里的整理是指当我们释放时就会将原先不连续的空间向前移动到一起去形成连续的空间,使得内存更为紧凑,这样就可以避免产生内存碎片的问题

优点
  • 完善了内存碎片的问题
缺点
  • 由于涉及到对象内存的移动,效率则会随之降低,速度变慢(对象的引用地址的改变、内存区块的拷贝移动)

3.复制算法(copy)

含有两个内存区,一个内存区正常存放对象,另一个为空区,首先会标记第一个区中的需要回收的对象,然后将第一个区中无需被回收的强引用对象复制到第二个空区中同时整理移动到一起,然后将第一个需要进行回收的内存区直接全部清空,最后交换这两个内存区的位置

优点
  • 不再产生内存碎片
缺点
  • 会占用两倍的内存空间

分代回收

JVM将内存区划分为两块,一块叫新生代,一块叫老年代,新生代中划分了三个区域分别是:伊甸园,幸存区From,幸存区To

长时间进行使用的对象就会放置到老年代中
使用结束后就可以丢弃的放置到新生代中
以此针对不同的策略进行垃圾回收

工作方式

第一次回收
  • 当我们创建新对象时,会将对象加入新生代中的伊甸园中 ,直到伊甸园被占满
  • 当伊甸园被占满后触发垃圾回收(Minor GC,minor gc 会停止其他用户线程,直到回收结束,才会继续启用其他用户线程),采用可达性分析判断对象的引用方式,(采用复制算法)然后标记出可回收的对象,将不可回收的对象复制移动到幸存区To中,再将这些不可回收对象的生命值+1(从0到1),这时候伊甸园的对象就会被直接回收掉
  • 最后将幸存区From和幸存区To进行交换位置
  • 后续就可以继续分配对象
第二至15次回收
  • 此时我们的伊甸园再次被占满,会和第一次一样标记出伊甸园中的需要回收的对象,同时检查幸存区From,查找是否有此次可以回收的对象然后进行标记
  • 标记完后,将不被回收的对象复制到幸存区To中(伊甸园和幸存区From都复制),增加1点生命值,回收掉伊甸园和,幸存区From中的对象,再将幸存区交换
第15次垃圾回收
  • 当一个对象经历15次垃圾回收任然存活,此时当前对象的生命值为15(4 bit),达到了边界阈值,说明当前对象的价值性极高,此时就会将幸存区From中生命值达到15的对象移动到老年代中
  • 直至老年代占满
老年代和新生代占满时触发的垃圾回收

此时新生代和老年代的空间都被占满就会先触发Minor GC,若还是不足则触发“老年代”的回收(Full GC)
那么Full GC就会对新生代和老年代一起进行清理!(此时回收的时间会更长)

若Full GC后内存仍然不足,会报OutofMemoryError

VM Option参数

参数说明
-Xms堆初始大小
-Xmx或-XX:MaxHeapSize=size堆最大大小
-Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size )新生代大小
-XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy幸存区比例(动态)
-XX:SurvivorRatio=ratio幸存区比例
-XX:MaxTenuringThreshold=threshold晋升阈值
-XX:+PrintTenuringDistribution晋升详情
-XX:+PrintGCDetails -verbose:gcGC详情
-XX:+ScavengeBeforeFullGCFullGC前MinorGC

垃圾回收器

串行垃圾回收器

单线程的垃圾回收器
适用于堆内存较小的,个人电脑的场景

开启
-XX:+UseSerialGC = Serial + SerialOld

Serial :工作在新生代,使用复制算法
SerialOld:工作在老年代,使用标记整理算法

串行垃圾回收器中工作时有一个安全点,当串行垃圾回收器进行工作时,所有CPU中运行的线程会在安全点被阻塞,只有垃圾回收线程运行,垃圾回收线程完成后其他线程继续运行(串行垃圾回收线程只有一个线程,单体工作,你只要想成串行电路就懂了)

吞吐量优先的垃圾回收器

多线程的垃圾回收器
适用于堆内存较大,有多核cpu支持,服务器场景
让单位时间内,回收的线程暂停的时间达到最短(垃圾回收时间占比单位时间的占比越低,吞吐量越高)

开启
-XX:+UseParallelGC :新生代吞吐量优先,Java8默认开启,采用复制算法
-XX:+UseParallelOldGC :老年代,采用标记整理算法
-XX:+UseAdaptivesizePolicy:设置自适应新生代垃圾回收策略(动态调整伊甸园、堆、晋升阈值等)
-XX:GCTimeRatio=ratio:设置垃圾回收时堆区大小,调整垃圾回收时间和总时间的占比(公式:1/1+ratio[默认99])
若工作100秒则只有1秒进行垃圾回收,若达不到则调整堆大小来达到预设目标(多为扩大堆的空间)
-XX:MaxGCPauseMillis=ms :设置垃圾回收最大暂停的毫秒数(默认200ms,若堆区增大,所需回收时间越长)
-XX:Paralle1GCThreads=n :设置垃圾回收的线程数

同样有个安全点,CPU中运行的线程会在安全点停止,此时所有线程一起进行垃圾回收,结束后再继续(垃圾回收线程数 ≈ CPU核数)

响应时间优先(CMS)

多线程的垃圾回收器
适用于堆内存较大,有多核cpu支持,服务器场景
尽可能让单次垃圾回收时的线程暂停的时间达到最短

开启
-XX :+UseConcMarkSweepGC:基于标记清除算法的垃圾回收,是多线程并发的,作用于老年代,若出现问题退化为Serial0ld
-XX:+UseParNewGC :作用于新生代
-XX:ParallelGCThreads=n :并行垃圾回收线程数≈CPU核数
-XX:ConcGCThreads=threads:设置并发垃圾回收线程数,建议为并行的1/4
-XX:CMSInitiating0ccupancyFraction=percent:按照比例设置CMS回收时机
(CMS在垃圾清理时会有浮动垃圾产生,所以要预留一定的空间)
-XX:+CNSScavengeBeforeRemark:在进行重新标记之前对新生代进行垃圾回收
以防新生代大量引用老年代的对象导致性能变差,缓解重新标记时的压力

老年代出现内存不足时:
多个CPU中的用户线程运行至安全点1暂停,对线程进行初始标记,初始标记完成后进入新的安全点2,所有线程继续工作,对初始标记的线程进行并发标记,并发标记后进入安全点3,然后进行重新标记,重新标记结束进入安全点4,结束后所有线程继续运行,对重新标记好的线程进行并发清理

垃圾回收器的退化

由于使用标记清除算法,所以CMS会产生碎片空间,当碎片过多导致的无法分配足够的空间给新对象时,会造成并发失败,此时ConcMarkSweepGC就会退化为使用串行垃圾回收的Serial0ld,进行一次串行的垃圾回收,这时候会花费很长的时间进行清理,此为CMS的缺点

Garbage First(G1)垃圾回收器

JDK7官方支持,JDK9中默认采用(9中取代CMS)
适用场景:

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200 ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记+整理算法,两个区域之间是复制算法
开启
-XX:+UseG1GC-XX:G1HeapRegionsize=size:
-XX:MaxGCPauseMillis=time:
垃圾回收阶段

在这里插入图片描述

young collection

G1将整个堆内存划分为大小相等的多个区域,每个区域都可以单独作为伊甸园,幸存区,老年代,巨型对象区(巨型对象可能会同时占多个区)
当类加载时产生的新对象会被分配到伊甸园中,当伊甸园中区域被占满会触发新生代的垃圾回收,使用复制算法将幸存对象拷贝到幸存区,待到幸存区逐渐被占满再次进行垃圾回收,此时判断满足晋升阈值的复制到老年代中,未达到则拷贝到另一个幸存区

young collection跨代引用

当通过跟对象查找新生代对象时,若根对象来源于老年代,则会对老年代进行遍历,但是通常老年代中对象很多,因此遍历起来耗时很长,这样效率很低,所以对老年代采用卡表技术,对老年代进行区域划分,每个卡表区域为512kb,若老年代中的某个卡表引用了新生代对象,则将此表标记为脏卡,那么在进行GC Root遍历时直接遍历脏卡即可

而新生代中有个记录集合(Remember Set)记录外部对其对象的引用,在进行新生代垃圾回收时就可以通过该集合锁定老年代中的引用对象所在的脏卡,有效减少GC Root的时间
脏卡的标记是通过写屏障(post-write barrier)完成的,在引用对象出现变更时进行更新,此更新为异步操作,会放置到脏卡更新队列(dirty card queue)中,由单独的脏卡更新线程进行更新

young collection + CM

在Young GC时会进行GC Root的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会暂停所有线程),由JVM参数决定:-XX:InitiatingHeapOccupancyPercent=percent(默认45%)

mixed collection

会对伊甸园、幸存区、老年代全面进行垃圾回收
最终标记(remark)进行STW(所有线程暂停)
拷贝存活(evacuation)也会进行STW
为达到-XX:MaxGCPauseMillis=ms设置的目标,G1会选择出老年代中回收价值最高的几个区进行垃圾回收

remark重标记

防止对象引用在并行阶段改变导致的问题
在对象引用发生改变时使用写前屏障(pre-write barrier),将该对象加入待处理队列(satb-mark queue)中,同时对该对象进行标记,直到并发标记结束进入STW,此时重新标记开始,重新标记阶段会将该待处理队列中的对象进行检查,发现对象被标记则将其进行判断处理,若发现有强引用对其进行引用,则不对其进行垃圾回收,否则认为可以进行垃圾回收

G1的字符串去重

若出现两个String对象new出来是一样的,则直接引用同一块内存区域
在这里插入图片描述

将所有新分配的字符串放入一个队列
当新生代回收时,G1并发检查是否有字符串重复
如果它们值一样,让它们引用同一个char[]
注意,与string.intern()不一样
String.intern()关注的是字符串对象
而字符串去重关注的是char[]
在JVM内部,使用了不同的字符串表

优点:节省大量内存
缺点:略微多占用了cpu时间,新生代回收时间略微增加

-XX:+UseStringDeduplication

并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingwithConcurrentMark 默认启用

回收巨型对象

—个对象大于region的一半时,称之为巨型对象
G1不会对巨型对象进行拷贝
回收时被优先考虑
G1会跟踪老年代所有脏卡(incoming)引用,这样老年代 incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK9并发标记起始时间的调整

并发标记必须在堆空间占满前完成,否则退化为FullGC
JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent
JDK 9可以动态调整
-XX:InitiatingHeapOccupancyPercent用来设置初始值
进行数据采样并动态调整
总会添加一个安全的空档空间

更多内容地址

以下为jdk18的G1地址
https://docs.oracle.com/en/java/javase/18/gctuning/introduction-garbage-collection-tuning.html#GUID-326EB4CF-8C8C-4267-8355-21AB04F0D304

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值