1、GC详解
GC的作用域
口诀:关于垃圾回收分代收集算法 : 不同的区域使用不同的算法
Young代:GC频繁区域
Old代:GC次数较少
Perm代:不会产生GC!一个对象的历程
Young = Eden + s0(from) + s1 (to)
JVM在进行GC时,并非每次都是对三个区域进行扫描的!大部分的时候都是在新生代PSYoungGen!
两个类型:
普通GC:新生代满了,只针对新生代PSYoungGen 【GC】
全局GC:老年代满了,主要是针对老年代ParOldGen,偶尔伴随新生代! 【Full GC】
可以看到普通GC针对新生代PSYoungGen进行垃圾回收,Full GC 针对老年代ParOldGen 和新生代PSYoungGen进行垃圾回收
GC面试题
1、JVM内存模型,每个区中存放什么?
方法区(1.7是永久代实现,1.8移除永久代,使用元空间实现)存放JVM加载的类信息,常量,字符串、静态变量
堆,存放new创建的对象和数组
栈,存放程序运行时的基本类型的变量和对象的引用变量
2、堆中的分区:Eden、Surival( form to)、老年代,请你说说他的特点?
所有的对象都是在Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入Surival(幸存者区),……. 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入老年代!运行了几个月之后,老年代满了,就会触发一次 Full GC;99% 的对象在 Eden 都是临时对象;
3、GC的三种收集方法:复制算法(新生代GC算法),标记清除、标记整理(压缩)(老年代GC算法),请你谈谈他的特点?
2、GC四大算法
引用计数法(了解即可)
特点:每个对象都有一个引用计数器,每当对象被引用一次,计数器就+1,如果引用失效,则计数器-1,如果为0,则GC可以清理;
缺点:
- 计数器维护麻烦!
- 循环引用无法处理!(互相引用的对象)
JVM 一般不采用这种方式
闲聊:现在一般使用可达性算法,GC Root(对不在引用的对象进行垃圾回收)
复制算法
在新生代中,就是使用复制算法的!
第一次GC后,Eden存活着一些对象,复制到s0区,Eden就是空的了
第二次GC后,Eden和s0都存活着一些对象,全部复制到s1
这时候s0和s1会进行位置互换,这是一个动态变化的区域,也就是所谓的from区和to区,记住谁空谁是to,该算法会保证s0和s1永远会有一个区是空的,它就是to区
这时候每次GC后存活的对象都会从Eden和from区复制到to区,原来的from区清空成为下一次GC的to区,自然现在有存活对象的to区就是from区了,这就是所谓的位置互换
1、一般普通GC 之后,差不多Eden几乎都是空的了!
2、每次存活的对象,都会被从 Eden 区和 from 区等复制到 to区,from 和 to 会发生一次位置交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15(默认值也是最大值,后面我们会讲解调整, ),就会进入老年代!
优点:
-
没有标记和清除的过程!效率高!没有内存碎片!
-
Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!1%的对象就是复制到to区,推荐使用 ==复制算法==
缺点:to区永远是空的,需要浪费双倍的空间
标记清除算法
老年代一般使用这个,但是会和我们后面的整理压缩一起使用!
优点:不需要额外的空间
缺点:两次扫描(第1次扫描标记存活对象,第2次扫描清除没有标记的对象),老年代的空间一般比新生代的空间大一倍以上,要扫描整个空间,耗时较为严重,也会产生内存碎片,不连续!
优化一下算法:整理压缩,将活着对象滑动到一侧,保证空间的完整性,避免了内存碎片
发现与复制算法的优缺点刚好相反
标记清除压缩
减少了上面标记清除的缺点:没有内存碎片!但是耗时可能也较为严重!
那我们什么时候可以考虑使用这个算法呢?
在我们这个要使用算法的空间中,假设这个空间中很少,不经常发生GC,那么可以考虑使用这个算法!分代清除算法
-
内存效率:复制算法 > 标记清除算法 > 标记整理(时间复杂度!)
-
内存整齐度:复制算法=标记整理>标记清除算法
-
内存利用率:标记整理 = 标记清除算法 > 复制算法
从效率来说,复制算法最好,但是空间浪费较多!为了兼顾所有指标,标记整理会平滑一点,但是效率不尽人意!
难道就没有一种最优的算法吗?思考一下:
答案:没有!没有最好的,只有最合适的!=> 分代收集算法:不同的区域使用不同的算法!
新生代
相对于老年区,对象存活率低!Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!
推荐使用 ==复制算法==
老年代
区域比较大,对象存活率较高!不经常发生GC
推荐使用标记清除压缩
天上飞的理念,都会有落地的是实现!
GC算法就是理论,垃圾回收器就是落地的实现
3、GC Root(根对象)
JVM 垃圾回收的时候如何确定垃圾,简单地说,就是不再被引用的对象!
Person person = null; // 不再被引用的对象
如果我们要进行垃圾回收,第一步:判断这个对象是否可以回收!
-
引用计数法
Java中,引用和对象都是有关联的,如果要操作对象,就要通过引用进行;
-
可达性分析算法
什么是GC Root,根有4种
1、虚拟机栈中引用的对象!栈中不可能存在垃圾
2、类中静态属性引用的对象
3、方法区中的常量
4、本地方法栈中 Native 方法引用的对象!
public class GCRoots{
// private byte[] array = new byte[100*1024*1024]; // 开辟内存空间!在堆里
// private static GCRoots2 t2; // GC root;
// private static final GCRoots3 t3 = new GCRoots3(); // GC root;
// 引用远远不止于此,强引用,软引用,弱引用,虚引用! 四个类的使用!
public static void m1(){
GCRoots g1 = new GCRoots(); //GC root 根对象
System.gc();
}
public static void main(String[] args){
m1();
}
}
5、OOM的六种场景
栈溢出
1、java.lang.StackOverflowError
// 方法不断递归调用自己,栈溢出
public static void main(String[] args) {
a();
}
public static void a(){
a();
}
堆内存溢出
2、java.lang.OutOfMemoryError: Java heap space
// -Xms8m -Xmx8m
public class OomDemo {
public static void main(String[] args) {
String str = "Coding";
while (true){
str += str + new Random(1111111111) + new Random(1111111111);
}
}
}
/*
* -Xmx8m -Xms8m -XX:+PrintGCDetails
*
* 分析GC日志:[GC (System.gc()) [PSYoungGen: 1344K->496K(2048K)] 1344K->536K(7680K), 0.0008573 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
* 1、GC 类型 GC:普通的GC,Full GC :重GC
* 2、1344K 执行 GC之前的大小
* 3、496K 执行 GC之后的大小
* 4、(2048K) young 的total大小
* 5、0.0012643 secs 清理的时间
* 6、user 总计GC所占用CPU的时间 sys OS调用等待的时间 real 应用暂停的时间
*
* GC :串行执行 STW(Stop The World理论,暂停整个世界,就是我在GC的时候,程序是不能运行的,直到我收完垃圾,程序才恢复运行)
并行执行 G1
*/
public class Demo02 {
public static void main(String[] args) {
System.gc(); // 手动唤醒GC(),但实际上它不会马上GC,它会等待cpu的调用
String str = "ilovecoding";
while (true){
str += str + new Random().nextInt(999999999)
+ new Random().nextInt(999999999);
}
// 最终出现问题:java.lang.OutOfMemoryError: Java heap space
}
}
执行结果:
可以看到触发了多次GC(轻) 和 Full GC(重),每次Full GC,都会把新生区清零
GC回收时间过长
3、java.lang.OutOfMemoryError: GC overhead limit exceeded
GC 回收时间过长也会导致 OOM;
可能CPU占用率一直是100%,GC 但是没有什么效果!因为它已经清理不出什么了
// -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
public class OomDemo {
public static void main(String[] args) throws Throwable {
int i = 0;
List<String> list = new ArrayList<String>();
try {
while (true){
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) { // 会抛出Error,Exception无法捕获,要使用父类 Throwable
System.out.println("i=>"+i);
e.printStackTrace();
throw e;
}
}
}
执行:
可以发现触发了很多次Full GC 重GC,在循环到149171次后报出OOM异常GC overhead limit exceeded,就是GC垃圾来不及回收,就是OOM的增长速度比GC垃圾回收的速度还要快
基础缓冲区溢出
4、java.lang.OutOfMemoryError: Direct buffer memory
基础缓冲区的错误!
// -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
public class OomDemo {
public static void main(String[] args) throws Throwable {
System.out.println("配置的MaxDirectMemorySize"
+VM.maxDirectMemory()/(double)1024/1024+"MB");
TimeUnit.SECONDS.sleep(2);
// 故意破坏!我们只设置了本地内存最大5m,创建一个6m大的byteBuffer,那它就溢出了
// ByteBuffer.allocate(); 分配JVM的堆内存,属于GC管辖
// ByteBuffer.allocateDirect() ; // 分配本地OS内存,不属于GC管辖
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
// java.lang.OutOfMemoryError: Direct buffer memory
}
}
执行结果:
因为kafka是java写的,它出现过这样的案例内存溢出,看是否添加了参数-XX:+DisableExplicitGC标志将System.gc() 系统唤醒GC 禁用了,或者可以把MaxDirectMemorySize 基础缓冲区调大
文章链接:https://blog.csdn.net/dreamweaver_zhou/article/details/101269443
解决方案:
# kafka根目录
cd /opt/kafka/bin
vi kafka-run-class.sh
去掉参数-XX:+DisableExplicitGC
添加参数-XX:MaxDirectMemorySize
了解一下-XX:+DisableExplicitGC 与-XX:MaxDirectMemorySize
ByteBuffer有两种:
-
heap ByteBuffer -> -XX:Xmx 一种是heap ByteBuffer,该类对象分配在JVM的堆内存里面,直接由Java虚拟机负责垃圾回收
JVM堆内存大小可以通过-Xmx来设置
-
direct ByteBuffer -> -XX:MaxDirectMemorySize 一种是direct ByteBuffer是通过JNI(Java Native Interface) 本地方法接口在JVM外分配内存的(JVM无法触碰到本地方法的)。通过jmap无法查看该快内存的使用情况。只能通过top来看它的内存使用情况。
direct ByteBuffer可以通过-XX:MaxDirectMemorySize来设置,此参数的含义是当direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),
在程序中可以获得-XX:MaxDirectMemorySize的设置的值。
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
public class MaxDirectMemorySize {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
System.out.println("maxMemoryValue:"+sun.misc.VM.maxDirectMemory());
System.out.println("================================");
ByteBuffer buffer=ByteBuffer.allocateDirect(0);
Class<?> c = Class.forName("java.nio.Bits");
Field maxMemory = c.getDeclaredField("maxMemory");
maxMemory.setAccessible(true);
synchronized (c) {
Long maxMemoryValue = (Long)maxMemory.get(null);
System.out.println("maxMemoryValue:"+maxMemoryValue);
}
}
}
执行结果:
注意
direct ByteBuffer通过full gc来回收内存的,direct ByteBuffer会自己检测情况而调用System.gc()
唤醒垃圾回收但不会马上执行等待CPU调度,但是如果参数中使用了-XX:+DisableExplicitGC
那么就无法回收该快内存了,它会自动将System.gc()
调用转换成一个空操作,就是应用中调用System.gc()
会变成一个空操作,如果设置了就需要我们手动来回收内存。不回收内存就就出现内存溢出
无法创建本地线程
5、java.lang.OutOfMemoryError: unable to create native Thread
高并发 , unable to create native Thread这个错误更多的时候和平台有关!
1、应用创建的线程太多!
2、服务器不允许你创建这么多线程!
java 是调用本地方法去创建线程的
public class TDemo {
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();
}
}
}
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
1、服务器线程不够了,超过了限制,也会爆出OOM异常!
# 编译
javac TDemo.java
# 执行代码
java TDemo
执行结果:
场景:一个用户开一个线程处理,一个秒杀活动,好几万用户进来,咔叽,服务爆内存溢出,无法创建本地线程。
元空间溢出
6、java.lang.OutOfMemoryError: Metaspace
java8 之后使用元空间代替永久代;本地内存!
存储以下东西:
1、虚拟机加载类信息
2、常量池
3、静态变量
4、编译后的代码
模拟元空间溢出、不断的生成类即可!
import org.springframework.cglib.proxy.Enhancer;
// -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
public class OomDemo {
static class OOMTest{}
public static void main(String[] args) throws Throwable {
int i = 0; // 模拟计数器
try {
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 (Throwable e) {
System.out.println("i=>"+i);
e.printStackTrace();
}
}
}
执行结果:
6、垃圾回收器
GC算法(引用计数、复制、标记清除、标记整理算法)方法论,垃圾收集器就是对应的落地实现!
垃圾收集器就是对应的GC算法的落地实现!
串行垃圾回收器
串行(STW:Stop the World)单个GC线程,触发GC,所有在运行的线程都停下,等待GC完成
并行垃圾回收器
多个GC线程工作,也会导致 STW,其他线程都必须停下来),
并发垃圾回收器
在回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行!
G1垃圾回收器
与前面3个垃圾回收器本质的区别,将堆内存分割成不同的区域,然后并发的对其进行垃圾回收,在java9之后,默认使用G1垃圾回收器
前面3个垃圾回收器的特点:
- 年轻代和老年代是各自独立的内存区域
- 年轻代使用 eden+s0+s1 复制算法
- 老年代使用标记清除压缩算法,必须扫描整个老年代的区域;
回收原理
垃圾回收器原则:尽可能少而快的执行GC为设计原则!
G1(Garbage-First)收集器 ,面向服务器端应用的收集器;
核心:将堆中内存区域(eden、s0、s1、old)打散,默认是2048块,
流程:把每个区域中gc后的存活对象复制到空格子(区域),原区域清空释放内存,而且是并行的
使用
-XX:+UseG1GC
自定义垃圾回收时间
这个还不是它最大的亮点,它增加了一些参数,可以自定义垃圾回收的时间!
# 最大的GC停顿时间单位:毫秒,JVM尽可能的保证停顿小于这个时间!
-XX:MaxGCPauseMillis=100
优点:
- 没有内存碎片
- 可以精准空垃圾回收时间
我们所使用的这个JVM环境,在不断的被优化! 进化,会越来越简单和高效===> 算法的进阶!
程序 = 数据结构 + 算法!
如何选择垃圾回收器
java 的 gc 回收器主要有哪些?曾经有7种,现在6种
- DefNew:默认的新生代 =【Serial 串行】
- Tenured:老年代 =【Serial Old】
- ParNew:并行新生代 = 【并行ParNew】
- PSYoungGen:并行清除新生区 = 上图的【Parallel Scavcegn】
- ParOldGen:并行老年区 = 上图的【Parallel Old】
查看java的默认垃圾回收器
# 查看默认的垃圾回收器,java8默认并行垃圾回收器,java9默认G1垃圾回收器
java -XX:+PrintCommandLineFlags -version
看一个jdk8的垃圾回收过程中
从图中看出jdk8 GC过程中使用的垃圾回收器是PSYoungGen
和ParOldGen
正好对应Parallel Scavcegn
和Parallel Old
使用串行垃圾回收器
发现垃圾回收详细打印的是Tenured 和 DefNew
# 串行GC,新生区使用DefNew + 老年区 Tenured
-XX:+UseSerialGC
# 并行GC,新生区使用ParNew + 老年区 Tenured
-XX:+UseParNewGC
# JDK8默认的垃圾回收器,新生代PSYoungGen(复制算法) + 老年代ParOldGen(标记清除压缩算法)
-XX:+UseParallelGC
# JDK9默认的垃圾回收器
-XX:+UseG1GC
Server / Client 模式
默认现在都是 Server 模式;Client几乎不会使用;
32位的Window操作系统,默认都是 Client 的 JVM 模式;
64位的默认都是 Server 模式;
选择合适的垃圾回收器
-
1、单CPU,单机程序,内存小
使用串行
-XX:UseSerialGC
-
2、多CPU,大的吞吐量、后台计算
使用并行
-XX:+UseParallelGC
-
3、多CPU,但是不希望有时间停顿,快速响应!
使用
-XX:+UseParNewGC
或者XX:+UseParallelGC
G1 优点
7、引用
强引用
默认的支持方式,假设出现了异常情况和OOM,只要是强引用的对象,都不会被回收!
强引用就是导致内存泄漏的原因之一!
//-Xms5m -Xmx5m -XX:+PrintGCDetails
public class Demo01 {
public static void main(String[] args) {
Object o1 = new Object(); // 这养定义的默认就是强引用
Object o2 = o1; // 真正的Object 对象信息存在堆中,o1和o2就是引用变量,存在JVM栈中
o1=null;
try {
byte[] bytes = new byte[10 * 1024 * 1024];
}catch (Throwable e){
e.printStackTrace();
}
finally {
System.out.println(o1); // null
System.out.println(o2); // java.lang.Object@6ff3c5b5
}
}
}
设置vm参数,执行结果:
发现OOM内存溢出了,强引用的对象依然不会被回收
软引用
相对于强引用弱化了,如果内存充足的化,GC不会回收该对象,但是内存不足的情况下,就会回收该对象
public class Demo02 {
public static void main(String[] args) {
Object o1 = new Object(); // 这养定义的默认就是强引用
SoftReference<Object> o2 = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(o2.get()); // 得到引用的值
o1=null;
System.gc();
System.out.println(o1); // null
System.out.println(o2.get()); // java.lang.Object@6ff3c5b5
}
}
执行结果:
修改为
//-Xms5m -Xmx5m -XX:+PrintGCDetails
public class Demo02 {
public static void main(String[] args) {
Object o1 = new Object(); // 这养定义的默认就是强引用
SoftReference<Object> o2 = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(o2.get()); // 得到引用的值
o1=null;
try {
byte[] bytes = new byte[10 * 1024 * 1024];
}catch (Throwable e){
e.printStackTrace();
}
finally {
System.out.println(o1); // null
System.out.println(o2); // java.lang.Object@6ff3c5b5
}
}
}
执行结果:
内存不足了,gc会回收软引用的对象,
结论:使用软引用能节省堆内存
弱引用
不论内存是否充足,只要是GC,就会回收该对象
public class Demo03 {
public static void main(String[] args) {
Object o1 = new Object(); // 这养定义的默认就是强引用
WeakReference<Object> o2 = new WeakReference<>(o1);
System.out.println(o1);
System.out.println(o2.get()); // 得到引用的值
o1=null;
System.gc();
System.out.println(o1);
System.out.println(o2.get()); // 得到引用的值
}
}
执行结果:
软引用和弱引用的使用场景
假设现在有一个应用,读取大量的本地图片
1、如果每次都从硬盘上读取,影响性能
2、一次加载到内存,可能造成内存溢出
我们的思路:
1、使用一个HashMap保存图片的路径和内容
2、内存足够,不清理
3、内存不足够,清理加载到内存中的数据
HashMap<String,SoftReference<Pic>> stringSoftReferenceHashMap = new HashMap<>();
虚引用(幽灵引用)
主要作用:跟踪对象的垃圾回收状态,
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object(); // 这样定义的默认就是强引用
// 虚引用 PhantomReference 需要结合队列使用
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);
System.out.println(o1); //
System.out.println(phantomReference.get()); // null
System.out.println(referenceQueue.poll()); // null
o1=null;
System.gc();
TimeUnit.SECONDS.sleep(1000);
System.out.println(o1); // null
System.out.println(phantomReference.get()); // null
// 通知机制! 好比我们常用的消息队列,消息消费了,ACK
// 通过队列检测哪些对象被清理了,可以处理一些善后动作
System.out.println(referenceQueue.poll());
}
}