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 -version
java -help
java -showversion
标配参数
X参数
X参数只要了解即可,如下X参数用于修改JVM的运行模式。
-Xint # 解释执行
-Xcomp # 第一次使用就编译成本地的代码
-Xmixed # 混合模式(Java默认)
修改JVM运行模式
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:后面是+开头,说明已经开启了该参数。PrintGCDetails参数开启
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
查看用户修改过的配置的XX选项
常用的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<String> 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以后为默认的垃圾回收器。其原理如下图所示。
G1垃圾回收器原理
查看默认的垃圾回收器:
java -XX:+PrintCommandLineFlags -version
查看默认垃圾回收器
Java的垃圾回收器有哪些?
Java曾经由7种垃圾回收器,现在有6种。主要垃圾回收器的位置分布和关系如下图所示。
主要垃圾回收器的位置分布和关系
上图中,红色箭头表示新生区中使用了对应的垃圾回收器,在老年区只能使用对应箭头指向的垃圾回收器。蓝色箭头表示曾经的垃圾回收器有过的对应关系。
6种垃圾回收器名称分别是:
- DefNew : 默认的新一代 【Serial 串行】
- Tenured : 老年代 【Serial Old】
- ParNew : 并行新一代 【并行ParNew】
- PSYoungGen : 并行清除年轻代 【Parallel Scavcegn】
-
ParOldGen: 并行老年区
JDK8默认的垃圾回收器
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<Object> 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<Object> 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<Object> referenceQueue=new ReferenceQueue<>();
PhantomReference<Object> 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
}
}
网友评论