GC详解
GC的作用域
GC的作用域如下图所示。
关于垃圾回收,只需要记住分代回收算法,即不同的区域使用不同的算法。
不同区域的GC频率也不一样:
- 年轻代:GC频繁区域。
- 老年代:GC次数较少。
- 永久代:不会产生GC。
一个对象的历程
一个对象的历程的如下图所示。
JVM在进行GC时,并非每次都是对三个区域进行扫描的,大部分的时候都是对新生代进行GC。 GC有两种类型:
- 普通GC(GC):只针对新生代 。
- 全局GC(Full GC):主要是针对老年代,偶尔伴随新生代。
GC的四大算法
引用计数法
引用计数法只需要了解即可,JVM 一般不采用这种方式进行GC。它的原理如下图所示。
原理:每个对象都有一个引用计数器,每当对象被引用一次,计数器就+1,如果引用失效,计数器就-1,当计数器为0,则GC可以清理该对象。
缺点:
- 计数器维护比较麻烦。
- 循环引用无法处理。
复制算法
年轻代中GC使用的就是复制算法。
原理:
- 一般普通GC之后,Eden区几乎都是空的了。
- 每次存活的对象,都会被从from区和Eden区等复制到to区,from区和to区会发生一次交换,每当GC后幸存一次,就会导致这个对象的年龄+1,如果这个年龄值大于15(默认GC次数,可以修改),就会进入养老区。记住一个点就好,谁空谁是to。
复制算法的原理如下图所示。
优点:
- 没有标记和清除的过程,效率高。
- 不会产生内存碎片。
由于Eden区对象存活率极低!,据统计99% 对象都会在使用一次之后引用失效,因此在该区中推荐使用复制算法。
标记清除算法
老年代一般使用这个GC算法,但是会和后面的标记整理压缩算法一起使用。其原理如下图所示。
原理:
- 先扫描一次,对存活的对象进行标记。
- 再次扫描,回收没有被标记的对象。
优点:不需要额外的空间。
缺点:
- 需要两次扫描,耗时严重。
- 会产生内存碎片,导致内存空间不连续。
标记清除压缩算法
标记清除压缩算法,也叫标记整理算法,该算法是在标记清除算法的基础上进行改进的算法,解决了标记清除算法会产生内存碎片的问题,但是相应的耗时可能也较为严重。其原理如下图所示。
原理:
- 先扫描一次,对存活的对象进行标记。
- 第二次扫描,回收没有被标记的对象。
- 压缩,再次扫描,将活着的对象滑动到一侧,这样就能让空出的内存空间是连续的。
当一个空间很少发生GC,可以考虑使用此算法。
GC算法小结
内存效率:复制算法>标记清除算法>标记整理算法
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
从效率上来说,复制算法最好,但是空间浪费较多。为了兼顾所有的指标,标记整理算法会平滑一些,但是效率不尽如意。 实际上,所有的算法,无非就是以空间换时间或者以时间换空间。
没有最好的算法,只有最合适的算法。所以上面说的分代收集算法,并不是指一种算法,而是在不同的区域使用不同的算法。
综上所述:
- 年轻代,相对于老年代,对象存活率较低,特别是在Eden区,对象存活率极低,99% 对象都会在使用一次之后引用失效,因此推荐使用复制算法。
- 老年代,区域比较大,对象存活率较高,推荐使用标记清除压缩算法。
JVM 垃圾回收的时候如何确定垃圾?GC Roots又是什么?
什么是垃圾?简单的说,就是不再被引用的对象。,如:
Object object=null;
如果我们要进行垃圾回收,首先必须判断这个对象是否可以回收。 在Java中,引用和对象都是有关联的,如果要操作对象,就要通过引用来进行。
可达性分析算法
可达性分析算法,简单来说就是通过从GC Root这个对象开始一层层往下遍历,能够遍历到的对象就是可达的,不能被遍历到的对象就是不可达的,不可达对象就是要被回收的垃圾。其原理如下图所示。
一切都是从 GC Root 这个对象开始遍历的,只要在这里面的就不是垃圾,反之就是垃圾。
什么是GC Root?
- 虚拟机栈中引用的对象。
- 类中静态属性引用的对象。
- 方法区中的常量。
- 本地方法栈中Native方法引用的对象。
如下代码所示:
public class GCRoots{ private byte[] array = new byte[100*1024*1024]; // GC root,开辟内空间! private static GCRoots2 t2; // GC root; private static final GCRoots3 t3 = new GCRoots3(); // GC root; public static void m1(){ GCRoots g1 = new GCRoots(); //GCroot System.gc(); } public static void main(String[] args){ m1(); }}
总结:
- 对于数组,如果只是在类成员中进行定义而没有声明数组大小,不是GC Root;如果已经声明了数组大小,则是GC Root,因为此时它已经开辟了内存空间。
- 对于静态成员对象属性,只要定义了,不管初始化值是null还是new出了对象,都是GC Root。
JVM常用参数
JVM只有三种参数类型:标配参数、X参数,XX参数。
标配参数
标配参数是指在JVM各个版本之间都非常稳定,很少有变化的参数。如:
java -versionjava -helpjava -showversion
X参数
X参数只要了解即可,如下X参数用于修改JVM的运行模式。
-Xint # 解释执行-Xcomp # 第一次使用就编译成本地的代码-Xmixed # 混合模式(Java默认)
XX参数之布尔型(重点)
-XX: +或者-某个属性值, + 代表开启某个功能,- 表示关闭了某个功能。 如以下代码让程序睡眠21亿秒:
package com.wunian.gc;//jps -l 查看堆栈信息,获得当前java程序端口号//jinfo -flag PrintGCDetails 5360 查看运行中的java程序,某项虚拟机参数是否开启(输出+号表示开启,-表示关闭)//jinfo -flag MetaspaceSize 6312 查看元空间大小//jinfo -flag MaxTenuringThreshold 6312 查看控制新生代中对象需要经历多少次GC晋升到老年代,默认为15//jinfo -flags 6312 查看指定端口的所有信息//java -XX:+PrintFlagsInitial 查看java环境初始默认值public class GCDemo { public static void main(String[] args) throws InterruptedException { System.out.println("Hello World"); Thread.sleep(Integer.MAX_VALUE); }}
程序运行后,打开DOS窗口,执行jps -l命令查看堆栈信息。得到当前程序运行的端口号,再执行jinfo -flag PrintGCDetails 端口号命令来查看刚刚运行的Java程序的PrintGCDetails参数是否开启,如果输出参数-XX:后面是-开头,表示没有开启,+开头表示已经开启了。
关闭程序,在IDEA配置中添加JVM参数-XX:+PrintGCDetails,再次启动程序,使用刚才的命令再次查看一下PrintGCDetails参数是否开启,输出参数-XX:后面是+开头,说明已经开启了该参数。
XX参数之key=value型
设置元空间大小为128M:-XX:MetaspaceSize=128m
执行jinfo -flag MetaspaceSize 端口号可以查看指定程序的元空间大小。
设置进入老年区的存活年限(默认是15年):-XX:MaxTenuringThreshold=15
该参数主要是控制新生代需要经历多少次GC晋升到老年代中的最大阈值。在JVM中用4个bit存储(放在对象头中),所以其最大值是15。
执行jinfo -flag MaxTenuringThreshold可以查看进入老年区的存活年限。
查看某个端口的所有信息的默认值:jinfo -flags 端口号
-XX:+UseParallelGC表示默认使用的是并行GC回收器。
经典面试题:-Xms, -Xmx,是XX参数还是X参数?
1.-Xms表示设置初始堆的大小,等价于:-XX:InitialHeapSize。
2.-Xmx表示设置最大堆的大小,等价于:-XX:MaxHeapSize。
因此,-Xms, -Xmx是XX参数,这种写法只不过是语法糖,方便书写。
一般最常用的东西都是有语法糖的。
初始的默认值
查看Java 环境初始默认值:-XX:+PrintFlagsInitial,只要在这里面显示的值,都可以手动赋值,但是不建议修改,了解即可。
=表示是默认值。 :=表示值被修改过。
查看被修改过的值:
java -XX:+PrintFlagsFinal -Xss128k GCDemo # 查看被修改过的值!启动的时候判断
查看用户修改过的配置的XX选项:java -XX:+PrintCommandLineFlags -version
常用的JVM调优参数
- -Xms:设置初始堆的大小。
- -Xmx:设置最大堆的大小。
- -Xss:线程栈大小设置,默认为512k~1024k。
- -Xmn: 设置年轻代的大小,一般不用改动。
- -XX:MetaspsaceSize :设置元空间的大小,这个在本地内存中。
- -XX:+PrintGCDetails :输出详细的垃圾回收信息。
- -XX:SurvivorRatio:设置新生代中的 Eden/s0/s1空间的比例。例如: uintx SurvivorRatio = 8表示Eden:s0:s1 = 8:1:1 uintx SurvivorRatio = 4表示Eden:s0:s1 = 4:1:1
- -XX:NewRatio:设置年轻代与老年代的占比。例如: NewRatio = 2表示新生代:老年代=1:2,默认新生代整个堆的1/3。 NewRatio = 4表示新生代:老年代=1:4,默认新生代整个堆的1/5。
- -XX:MaxTenuringThreshold:进入老年区的存活阈值。例如: MaxTenuringThreshold = 15表示GC15次后存活的对象进入老年区。
常见的几种OOM
java.lang.StackOverflowError
栈溢出,最常见的OOM之一,方法调用自身,示例代码如下:
package com.wunian.gc;/** * 栈溢出 java.lang.StackOverflowError * 方法调用自身 */public class OOMDemo { public static void main(String[] args) { a(); } public static void a(){ a(); }}
java.lang.OutOfMemoryError: Java heap space
堆溢出,最常见的OOM之一,字符串无限拼接,示例代码如下:
package com.wunian.gc;import java.util.Random;/** * 堆溢出 java.lang.OutOfMemoryError: Java heap space * -Xms10m -Xmx10m */public class OOMDemo2 { public static void main(String[] args) { String str="coding"; while(true){ str+=str+new Random(1111111111)+new Random(1111111111); } }}
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收时间过长(次数过多)也会导致 OOM,可能CPU占用率一直是100%,频繁GC但是没有什么效果。示例代码如下:
package com.wunian.gc;import java.util.ArrayList;import java.util.List;/** * GC回收时间(次数)过长也会导致 OOM; java.lang.OutOfMemoryError: GC overhead limit exceeded * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails */public class OOMDemo3 { public static void main(String[] args) { int i=0; List list =new ArrayList<>(); try { while(true){ list.add(String.valueOf(++i).intern()); /** * String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。 * 当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String, * 若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String, * 然后返回这个String在常量池中的引用。 */ } } catch (Exception e) { System.out.println("i=>"+i); e.printStackTrace(); throw e; } }}
java.lang.OutOfMemoryError: Direct buffer memory
基础缓冲区错误,使用NIO方法分配的本地内存超出了JVM参数设置的最大堆外内存。设置最大Java堆外内存大小:-XX:MaxDirectMemorySize=5m,示例代码如下:
import sun.misc.VM;import java.nio.ByteBuffer;import java.util.concurrent.TimeUnit;/** * 基础缓冲区的错误! java.lang.OutOfMemoryError: Direct buffer memory * -XX:MaxDirectMemorySize可以设置java堆外内存的峰值 * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails */public class OOMDemo4 { public static void main(String[] args) throws InterruptedException { System.out.println("配置的MaxDirectMemorySize"+ VM.maxDirectMemory()/(double)1024/1024+"MB"); TimeUnit.SECONDS.sleep(2); //故意破坏 //ByteBuffer.allocate();分配 JVM的堆内存,属于GC管辖 //ByteBuffer.allocateDirect();//分配本地OS内存,不属于GC管辖 分配了6M内存,但是jvm参数设置了最大堆外内存是5M ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024); }}
java.lang.OutOfMemoryError: unable to create new native thread
高并发环境下,此错误更多的时候和平台有关,出现此错误的可能原因有:
- 应用创建的线程太多。
- 服务器不允许你创建这么多线程。
示例代码如下:
package com.wunian.gc;/** * 服务器线程不够了,超过了限制,也会爆出OOM异常 * java.lang.OutOfMemoryError: unable to create new native thread */public class OOMDemo5 { public static void main(String[] args) { for (int i = 1; ; i++) { System.out.println("i=>"+i); new Thread(()->{ try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } },""+i).start(); } }}
java.lang.OutOfMemoryError: Metaspace
Java8之后使用元空间代替永久代,使用的是本地内存。元空间主要用于存储:
- 虚拟机加载类信息
- 常量池
- 静态变量
- 编译后的代码
要模拟元空间溢出,只需要不断的生成类即可,这里需要用到Spring中的Enhancer类,示例代码如下:
package com.wunian.gc;import org.springframework.cglib.proxy.Enhancer;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/** * 元空间溢出 java.lang.OutOfMemoryError: Metaspace * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m */public class OOMDemo6 { static class OOMTest{} public static void main(String[] args) { int i=0;//模拟计数器 try { //不断的加载对象!底层使用Spring的cglib动态代理 while (true) { i++; Enhancer enhancer=new Enhancer(); enhancer.setSuperclass(OOMTest.class); enhancer.setUseCache(false);//不使用缓存 enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return method.invoke(o,args); } }); enhancer.create(); } } catch (Exception e) { System.out.println("i=>"+i); e.printStackTrace(); } }}
深入理解垃圾回收器
GC算法如引用计数算法、复制算法、标记清除算法、标记整理算法都是方法论,垃圾回收器就是这些算法对应的落地的实现。
四种垃圾回收器
1、串行垃圾回收器,单线程工作,执行GC时会停止所有的线程直到GC结束(STW:Stop the World)。其原理如下图所示。
2、并行垃圾回收器,多线程工作,也会导致STW。其原理如下图所示。
3、并发垃圾回收器,在回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行。其原理如下图所示。
4、G1垃圾回收器,将堆内存分割成不同的区域,然后并发的对其进行垃圾回收。Java9以后为默认的垃圾回收器。其原理如下图所示。
查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version
Java的垃圾回收器有哪些?
Java曾经由7种垃圾回收器,现在有6种。主要垃圾回收器的位置分布和关系如下图所示。
上图中,红色箭头表示新生区中使用了对应的垃圾回收器,在老年区只能使用对应箭头指向的垃圾回收器。蓝色箭头表示曾经的垃圾回收器有过的对应关系。 6种垃圾回收器名称分别是:
- DefNew : 默认的新一代 【Serial 串行】
- Tenured : 老年代 【Serial Old】
- ParNew : 并行新一代 【并行ParNew】
- PSYoungGen : 并行清除年轻代 【Parallel Scavcegn】
- ParOldGen: 并行老年区
JVM的Server/Client模式
现在的JVM默认都是Server模式,Client几乎不会使用。以前32位的Windows操作系统,默认都是Client的 JVM 模式,64位的默认都是 Server模式。
垃圾回收器之间的组合关系
上述6种垃圾回收器都是组合使用的,新生区使用了某种垃圾回收器,养老区会使用与之对应的垃圾回收器,并不是自由搭配的。如下图所示。
如何选择垃圾回收器
1、单核CPU,单机程序,内存小。选择-XX:UseSerialGC。
2、多核CPU,吞吐量大,后台计算。选择XX:+UseParallelGC。
3、多核CPU,不希望有时间停顿,能够快速响应。选择-XX:+UseParNewGC 或者 XX:+UseParallelGC。
G1垃圾回收器
以往垃圾回收器的特点
1、年轻代和老年代是各自独立的内存区域。
2、年轻代使用Eden+s0+s1复制算法。
3、老年代垃圾收集必须扫描整个老年代的区域。
4、垃圾回收器原则:尽可能少而快的执行GC。
G1垃圾回收器的原理
G1(Garbage-First)垃圾回收器 ,是面向服务器端的应用的回收器。其原理如下图所示。
原理:将堆中的内存区域打散,默认分成2048块。不同的区间可以并行处理垃圾,在GC过程中,幸存的对象会复制到另一个空闲分区中,由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩)。
使用G1垃圾回收器:-XX:+UseG1GC
G1垃圾回收器最大的亮点是可以自定义垃圾回收的时间。设置最大的GC停顿时间(单位:毫秒):XX:MaxGCPauseMillis=100 ,JVM会尽可能的保证停顿小于这个时间。
G1垃圾回收器的优点
- 没有内存碎片。
- 可以精准的控制垃圾回收时间。
强引用、软引用,弱引用和虚引用
主要学习三个引用类:SoftReference、WeakReference和PhantomReference
强引用
假设出现了异常或OOM,只要是强引用的对象,都不会被回收。强引用就是导致内存泄露的原因之一。
package com.wunian.ref;/** * 强引用 * -XX:+PrintGCDetails -Xms5m -Xmx5m */public class StrongRefDemo { public static void main(String[] args) { Object o1=new Object();//这样定义的默认就是强引用 Object o2=o1; o1=null; System.gc(); System.out.println(o1);//null System.out.println(o2);//java.lang.Object@6e0be858 }}
软引用
相对于强引用弱化了。如果系统内存充足,GC不会回收该对象,但是内存不足的情况下就会回收该对象。
package com.wunian.ref;import java.lang.ref.SoftReference;/** * 软引用 * -XX:+PrintGCDetails -Xms5m -Xmx5m */public class SoftRefDemo { public static void main(String[] args) { Object o1=new Object();//这样定义的默认就是强引用 //Object o2=o1; SoftReference o2=new SoftReference<>(o1);//软引用 System.out.println(o1);//java.lang.Object@6e0be858 System.out.println(o2.get());//得到引用的值 java.lang.Object@6e0be858 o1=null; try { byte[] bytes=new byte[10*1024*1024]; } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(o1);//null System.out.println(o2.get());//null //由于堆内存不足被回收 } //System.gc(); }}
弱引用
不论内存是否充足,只要是GC就会回收该对象。
package com.wunian.ref;import java.lang.ref.WeakReference;/** * 弱引用 * -XX:+PrintGCDetails -Xms5m -Xmx5m */public class WeakRefDemo { public static void main(String[] args) { Object o1=new Object();//这样定义的默认就是强引用 WeakReference o2 = new WeakReference<>(o1); System.out.println(o1);//java.lang.Object@6e0be858 System.out.println(o2.get());//得到引用的值 java.lang.Object@6e0be858 o1=null; System.gc(); System.out.println(o1);//null System.out.println(o2.get());//null }}
软引用、弱引用的使用场景
假设现在有一个应用,需要读取大量的本地图片。
1、如果每次读取图片都要从硬盘中读取,影响性能。
2、一次加载到内存中,可能造成内存溢出。
我们的思路:
1、使用一个HashMap保存图片的路径和内容。
2、内存足够,不清理。
3、内存不足,清理加载到内存中的数据。
虚引用
虚就是虚无,虚引用就是没有这个引用。虚引用需要结合队列使用,其主要作用是跟踪对象的垃圾回收状态。
package com.wunian.ref;import java.lang.ref.PhantomReference;import java.lang.ref.ReferenceQueue;import java.util.concurrent.TimeUnit;/** * 虚引用 */public class PhantomRefDemo { public static void main(String[] args) throws InterruptedException { Object o1=new Object(); //虚引用需要结合队列使用 ReferenceQueue referenceQueue=new ReferenceQueue<>(); PhantomReference objectPhantomReference=new PhantomReference<>(o1,referenceQueue); System.out.println(o1);//java.lang.Object@6e0be858 System.out.println(objectPhantomReference.get());//null System.out.println(referenceQueue.poll());//null o1=null; System.gc(); TimeUnit.SECONDS.sleep(1); System.out.println(o1);//null System.out.println(objectPhantomReference.get());//null //这好比是一个垃圾桶,通过队列来检测哪些对象被清理了,可以处理一些善后工作 System.out.println(referenceQueue.poll());//java.lang.ref.PhantomReference@61bbe9ba }}