前面我们使用到了System.gc();
方法,可以回收垃圾。
那么他具体是怎样的呢?我们来看看吧。
可以看到他是C语言写的,但我现在要说的是,我们调用了System.gc();的话,垃圾并不是立马回收,而是告诉虚拟机需要进行回收了。
测试代码:
/**
* @author 龙小虬
* @date 2021/4/16 22:54
*/
public class SystemGC {
public static void main(String[] args) {
new SystemGC();
System.gc();
}
// 垃圾回收器回收的时候会走这个方法
@Override
protected void finalize() throws Throwable {
System.out.println("重写finalize方法");
}
}
它会有两种情况:
- 没有立马进行回收
- 立马回收
如果我们一定要立马回收呢?那就需要调用System.runFinalization(),即可。第一种情况是概率性的,如果想要真的看到结果,需要多次测试。
7种核心的收集器
- 串行回收器:Serial、Serial old
- 并行回收器:ParNew、Parallel Scavenge、Parallel old
- 并发回收器:CMS、G1(分区算法)
为什么有着这么多的垃圾回收器,第一个回收器在jdk1.3就出来了,之后在2017年频繁更新垃圾回收器,为什么?这是因为17年之后,微服务框架已经兴起了。为了更好的适应微服务框架的到来,所以不断地更新,那为什么这么频繁,他们有什么目的?目的主要就是我们前面文章提到的Stop-The-World机制,因为触发它会导致用户线程全部暂停不能正常的访问堆内存,所以为了更好地适应微服务,不断地想办法降低stw机制的阻塞时间。
这样我们也就了解了,我们需要怎么去在生产环境下调优JVM。
在生产环境下调优jvm
前面提到的垃圾回收器不断更新是因为stw机制,那么我们要怎么去减少机制的触发,从而使阻塞时间变短,这就是我们调优JVM的一个要点。
我们都知道,stw机制的触发是因为内存不足,或者扩容的情况下才会触发,所以我们在生产环境下,不要频繁的触发垃圾回收或者是降低用户线程阻塞的时间,因为所有垃圾回收的时候都会暂停用户线程。那怎么避免频繁的触发垃圾回收?
那肯定就是尽量不扩容嘛:
- 提升新生代、老年代的内存大小
- 生产环境中,初始容量和最大容量保持一致
- 生产环境中不要调用System.gc()
垃圾回收器与垃圾回收算法区别
- 垃圾收集器:串行、并行收集、CMS、G1\ZGC。能够降低对用户线程暂停的时间或者用户线程和GC线程同时运行
- 垃圾收集算法:标记清除、标记整理、标记复制、分代算法
垃圾收集器中包含了垃圾收集算法。
串行、并行的区别
- 串行就是GC单线程回收垃圾(单个线程回收)
优点:简单而高效
缺点:只有一个GC线程清理堆内存垃圾,一旦堆内存过大,从而导致stw时间过长
应用场景:堆内存比较小的项目或者是桌面应用程序 - 并行就是GC多线程方式回收垃圾(多个线程同时回收,java8默认使用并行收集器)
优点:如果堆内存空间比较大,采用并行收集器可以提高清理堆内存空间的效率
缺点:开启多个线程同时清理垃圾,可能会很消耗CPU资源
应用场景:多核多线程情况下
并发、并行的区别
并行、串行收集器当GC的线程在清理堆内存垃圾的时候,都会暂停用户线程
并发在GC的线程清理堆内存垃圾的时候,不会暂停用户线程(垃圾清理过程中会有非常短暂的暂停用户线程)代表:CMS、G1
怎么查看默认收集器
+代表使用了,-代表未使用
1.cmd命令
java -XX:+PrintCommandLineFlags -version
jinfo -flag UseParallelGC (线程pid)
2.idea
-XX:+PrintCommandLineFlags
3.jdk自带的jconsole
例如:
既然我们了解了默认的额垃圾回收器,那么就该知道一下垃圾回收的步骤
清理堆垃圾的步骤
根据GCRoot查找到整个引用链,只要有被GCRoot关联对象,则标记为可用对象,如果GCRoot引用链过长,那么就会导致遍历的时间过长,stw时间过长,这也是为什么串行、并行回收慢的原因
CMS收集器原理
-
初始标记(CMS initial mark)
标记GCRoot能直接关联的对象,但是也会让所有用户线程暂停,暂停用户线程时间非常短 -
并发标记(CMS concurrent mark)
用户线程与GC线程同时运行,此时根据初始标记得到直接引用对象,查询整个链与GCRoot能够关联的对象 -
重新标记(CMS remark)
在初始标记之后,在并发标记过程中,并发标记线程和用户线程同时运行中,有可能会发生引用改变,所以需要重新修正,并且暂停用户线程,但是时长会比并发标记时间短,比初始标记时间长 -
并发清除(CMS concurrent sweep)
用户线程和GC线程同时运行,GC线程同时运行清理堆内存垃圾,用户线程正常运行。并发清除过程中还会产生垃圾,因为它使用的是标记清除算法在这第四步,为什么不使用标记整理算法?标记整理算法会更改原对象的内存地址,但是现在的用户线程同时在运行,所以不能使用,否则会发生用户线程引用不到数据。cms核心理念:减少stw阻塞等待时间,如果使用那岂不是没有达到减少阻塞的等待时间的效果吗?
缺点:产生碎片化问题
处理方案:直接触发FullGC,采用老年代串行回收器清理整个堆内存垃圾,采用标记整理算法,导致所有用户线程阻塞。这也就是我们所说的,老年代使用CMS的时候,拥有备胎收集器老年代串行收集器
stw机制
那说了这么多的理念,怎么证明stw机制会导致线程暂时阻塞呢?
代码:
import java.util.ArrayList;
/**
* @author 龙小虬
* @date 2021/4/16 23:34
*/
public class STWBlock {
public static void main(String[] args) {
new MyThread().start();
new Thread(() -> {
ArrayList<byte[]> bytes = new ArrayList<>();
while (true) {
for (int i = 0; i < 20; i++) {
bytes.add(new byte[10 * 1024 * 1024]);
}
if (bytes.size() > 20) {
bytes.clear();
System.gc();
}
}
}).start();
}
static class MyThread extends Thread {
long startTime = System.currentTimeMillis();
@Override
public void run() {
while (true) {
try {
long end = System.currentTimeMillis();
System.out.println(end / 1000 + "." + end % 1000);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
在这里就发现,时间上的差距就越来越参差不齐了。
这也说明了,线程在某个时间会发生阻塞,才会导致这个问题。