1、四种垃圾回收算法
如何确定垃圾
- 引用计数:引用与对象相关联,如果要操作对象,则必须使用引用。因此,可以通过引用计数来确定对象是否可以回收。实现原则是,如果一个对象被引用一次,计数器 +1,反之亦然。当计数器为 0 时,该对象不被引用,则该对象被视为垃圾,并且可以被 GC 回收利用,但是引用计数算法无法解决循环引用问题
- 可达性分析:为了解决引用计数法的循环引用问题,Java 采用了可达性分析的方法。其实现原理是,将一系列"GCroot"对象作为搜索起点。如果在"GCroot"和一个对象之间没有可达的路径,则该对象被认为是不可访问的
- 引用计数(以上已有说明)
- 复制:如下图复制算法会划分两片区域,一片区域是已使用,一片区域是备用复制,当触发GC时,会先进行可达性分析,为需要GC的区域进行标记,再进行GC时会把已使用的区域复制到备用区域,此时两片区域功能性对换,这就是复制算法的本质
- 优点:实现简单内存效率高,不易产生碎片
- 缺点:内存被压缩一半,当活跃对象过多,复制效率会大大降低
- 标记清除:此算法分为两个阶段,首先标记出需要回收的对象,再标记完成后对已标记的对象统一清理回收
- 缺点:在清楚之后会产生大量不连续的内存碎片,这样很容易导致在创建新的大对象时,无法分配足够的连续空间导致再次触发GC操作
- 标记整理:此算法会先对内存中的对象进行分析,进行标记,此步骤和标记清除算法一致,在整理阶段会将把已使用的区域统一复制到内存的一段,再将剩余标记的对象进行清理
- 优点:不会产生内存碎片
- 缺点:耗时相对较长
分代收集算法是以上算法根据不同情况的回收区域进行最优选择,就不做赘述,后续在总结7种垃圾回收器会有分析算法使用情况
2、GCroot和四大引用类型
- GCroot
- 什么是GCroot:GCroot本质上是一个初始引用的集合,对象是否需要被回收,需要判断是否有一条路径从GCroot中出发,并指向目标对象,如果存在则表示该对象可达,不需要进行回收,反之则需要进行清理
- 如何判断是否属于GCroot集合(4种)
- 虚拟机栈,在栈帧中的本地变量表引用的对象
- native方法引用的对象
- 方法区中的静态变量
- 方法区常量引用的对象
- 四大引用
软引用、弱引用、虚引用都有构造函数可以传入一个引用队列用于记录被GC回收的引用对象
-
- 强引用(一般我们在方法里创建的对象即为强引用,这边就不做介绍)
- 软引用:在GC时若内存充足时不会被GC回收,只有在内存不足时才会被GC回收
/** * 内存充足时的软引用 */ private static void softReferenceEnoughDemo(){ Object o1 = new Object(); SoftReference<Object> softReference = new SoftReference<>(o1); System.out.println(o1); System.out.println(softReference.get()); // 将o1引用置为null o1 = null; // 手动触发gc收集 System.gc(); System.out.println("=============================="); System.out.println(o1); System.out.println(softReference.get()); } /** * 内存不足时的软引用 */ private static void softReferenceNotEnoughDemo(){ Object o1 = new Object(); SoftReference<Object> softReference = new SoftReference<>(o1); System.out.println(o1); System.out.println(softReference.get()); // 将o1引用置为null o1 = null; System.out.println("=============================="); try { byte[] bytes = new byte[20 * 1024 * 1024]; } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(o1); System.out.println(softReference.get()); } }
jvm参数配置下图,上图是执行控制台打印出的程序执行信息以及GC回收的详细信息
-Xms10m -Xmx20m -XX:+PrintGCDetails //使用jdk8默认的垃圾回收器
- 弱引用:弱引用只要触发GC就会被回收,与内存是否充足无关
private static void weakReferenceDemo(){ Object o1 = new Object(); WeakReference<Object> weakReference = new WeakReference<>(o1); System.out.println(o1); System.out.println(weakReference.get()); // 将o1引用置为null o1 = null; // 手动触发gc收集 System.gc(); System.out.println("=============================="); System.out.println(o1); System.out.println(weakReference.get()); }
- 虚引用:虚引用的使用一般需要结合ReferenceQueue使用,查看源码可以看到,虚引用的构造函数入参需要传入引用队列,虚引用在创建后在任何时候都可能回收,回收时会被放入引用队列,可以用来做类似aop中的后置通知的功能,用于监听销毁,即在一个finalization阶段可以被gc回收,用来实现再次之前做一些必要的操作(虚引用的get操作返回的永远是null,所以无法获取到引用对象)
/** * 虚引用 */ private static void phantomReferenceDemo(){ Object o1 = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue); System.out.println(o1); System.out.println(phantomReference.get()); System.out.println(referenceQueue.poll()); o1 = null; System.gc(); System.out.println(o1); System.out.println(phantomReference.get()); System.out.println(referenceQueue.poll()); }
3、JVM参数配置详解和常见的几种OOM类型
- 参数配置
- X类型(简称分类)
- XX类型(简称分类)
- 常见OOM类型(前两种相对常见,此次不贴代码)
- java.lang.StackOverflowError:栈内存溢出,常出现在错误的递归调用种
- java.lang.OutOfMemoryError:Java heap space:堆内存异常,对象创建超出了堆的最大值
- java.lang.OutOfMemoryError:GC overhead limit exceeded
/** * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m * 指定堆外最大内存,不设置默认是64m如果超出设置值,这会多次触发FullGC */ public static void main(String[] args) { Integer i = 0; List<String> list = new ArrayList<>(); try { while(true){ list.add(String.valueOf(++i).intern()); } } catch (Throwable e) { System.out.println("*************i:" + i); e.printStackTrace(); throw e; } }
GC回收时间过长则会抛出错误,GC时间过长的实际是指,超过98%的时间做GC并且回收了不到2%的堆内存,连续多次触发FullGC都只回收不到2%的极端情况下才会抛出;以上设置的参数(-XX:MaxDirectMemorySize=5m)限制对外内存5m,当对外内存达到上限则触发FullGC操作,而每次GC的效果不明显,再次触发FullGC,恶心循环,频繁触发GC操作,此时CPU使用率一直偏高,故抛出错误
- java.lang.OutOfMemoryError:Direct buffer memory:本地内存错误
public static void main(String[] args) { //-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m System.out.println(VM.maxDirectMemory() / 1024 / 1024 + "MB"); try { Thread.sleep(3000); }catch (Exception e){ e.printStackTrace(); } ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6*1024*1024); }
在NIO程序经常使用ByteBuffer来读取或者写入数据其中使用ByteBuffer.allocateDirect()方法可以直接分配本地内存,而本地内存不属于GC管辖范围,如果不断使用本地内存,而少量使用堆内存,由于堆内存充足,JVM不会触发GC操作那DirectByteBuffer对象们就不会回收,而此时再次分配本地内存就出现了OOM错误
- java.lang.OutOfMemoryError:unable to create new native thread:线程数量超出最大值,无法再创建新的线程(在linux环境中默认每个用户(root用户外)最大开启的线程数为1024,如果超出)
- java.lang.OutOfMemoryError:Metaspace
/** * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m */ static class test{} public static void main(final String[] args) { int i = 0; try { while(true){ i++; Enhancer enhancer =new Enhancer(); enhancer.setSuperclass(test.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invoke(o, args); } }); enhancer.create(); } }catch (Throwable e){ System.out.println("多少次之后发生了异常: " + i); e.printStackTrace(); } }
永久代(java8之后元空间取代了永久代)存放一下信息:1、虚拟机加载的类信息 2、常量池 3、静态变量 4、即时编译后的代码,如果元空间不断创建改大小以上信息则会出现OOM错误
4、6大垃圾回收器(JDK8,G1收集器单独总结)
- 参数预先说明
- DefNew-----Default New Generation
- Tenured-----Old
- ParNew-----Parallel New Generation
- PsYongGen-----Parallel Scavenge
- ParOldGen-----Parallel Old Generation
- Server/Client模式:在32位windows操作系统固定是Client模式,在32位骑它操作系统2g内存且同时有两个cpu上则位Server模式,低于该配置Client模式,64位only server模式
- 新生代使用的GC收集器(可以查看相应源码,年轻代和老年代的收集器匹配关系,这里就不贴相应源码)
- 串行GC:(Serial)/(Serial Copying) 使用参数-XX:UseSerialGC,则表示开启Serial(Young区使用)+Serial Old(Old区使用)的收集器组合,新生代、老年代都会使用串行回收器,新生代使用复制算法,老年代使用标记-整理算法
- 并行GC(ParNew)使用参数-XX:UseParNewGC,表示启用ParNew收集器,只影响新生代,老年代的收集器不发生改变,ParNew(Young区)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法,但是,ParNew+Tenured这样的搭配,java8不在推荐使用(注意:可以使用-XX:ParallelGCThreads 限制线程数量默认是cpu数量)
- 并行回收GC(Parallel)/(Parallel Scavenge):Parallel Scavenge类是ParNew,也是复制算法,吞吐量优先收集器,它能自适应调节策略,根据运行情况收集性能监控信息,动态调整这些参数提供最佳的停顿时间(-xx:MaxGCPauseMills),若使用参数-XX:UseParallelGC则在老年代收集器默认使用-XX:UseParallelOldGC,同ParNew一样也可以通过参数设置并线线程数
- 老年代(Serial Old 和 Parallel Old在年轻代介绍,以下就不展开)
- 串行(Serial Old)/(Serial MSC)
- 并行GC(Parallel Old)/(Parallel MSC)
- 并发标记清除GC(CMS):并发标记清楚最大的优点低停顿,可以与用户线程一起执行;在使用参数-XX:+UseConcMarkSweepGC后默认使用ParNew(Young区)+CMS(Old区)+Serial Old,其中Serial Old将作为CMS出错的备用收集器
- 4步过程:
- 初始标记(CMS initial mark):初始标记对象
- 并发标记(CMS concurrent mark):可以和用户线程一起并发执行
- 重新标记(CMS remark):二次确认由于用户线程执行而没有被标记的对象
- 并发清楚(CMS concurrent sewwp):可以和用户线程一起并发执行
- 优缺点
- 优势:并发收集且低停顿
- 缺点:并发执行cpu资源压力较大、采用标记清楚算法会导致大量内存碎片
- 4步过程:
- 总结
- 多cpu高吞吐使用:ParallelGC
- 多cpu低停顿,低延时响应快:CMS
5、G1垃圾回收器的深入理解
- 配置:-XX:+UseG1GC配置参数后发现Heap没有在像之前的GC收集器配置一样划分年轻代、老年代
- 简介说明:在官网描述中G1是一种服务器端的辣鸡收集器,主要应用在多处理器和大容量的内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停的时间的要求,在jdk9中默认使用G1替代CMS
- G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW(停顿)
- G1整体上采用标记整理算法,局部通过复制算法,不会产生内存碎片
- 宏观意义上G1不在划分年轻代和老年代,而是把内存划分为多个独立的子区域(Region),类比于hdfs的存储系统
- 在每个Region中保留了年轻代和老年代,每个Region组件不再是物理隔离,而是一些Region的集合且Region之间不需要连续,即不同的Region可以在执行不同的GC操作
- G1的分区也不存在老年代和年轻代的区别,在G1的运行下每个分区可能在不同代的状态下进行切换
- 底层原理:它并没有把在宏观意义上Heap划分位年轻代、老年代,取而代之的是将Heap区域划分为多个Region
- Region区域化:可以通过-XX:G1HeapRegionSize=32m,可以指定分区大小(1MB-32MB,必须是2的幂),默认将整堆划分为2048个分区
- 回收步骤:小区域收集+区域移动+筛选回收
- G1收集运行图(后续补充)
- 与CMS比较
- 不会产生内存碎片
- 保证高吞吐的同时,也保证了低停顿
6、扩展(JDK11中的zGC)
借鉴一款收费的GC收集器
后续学习再做补充。。。