文章目录
自动垃圾收集
概念:自动垃圾收集是查看堆内存,识别正在使用哪些对象以及哪些对象未被删除以及删除未使用对象的过程。
使用中的对象或引用的对象意味着程序的某些部分仍然维护指向改对象的指针。
程序的任何部分都不再引用未使用的对象或未使用的对象,因此可以回收尚未引用对象使用的内存。
像C语言中,分配和释放内存回一个手动的过程。
在java中,解除分配内存的过程由垃圾收集器自动处理。
一.如何确定内存需要被回收?
该过程的第一步就是需要被标记,这是垃圾搜集器在识别哪些内存正在使用而哪些不在使用的地方。
判断一个内存需不需要被回收有三种方法
- 对象回收 - 引用计数
- 对象回收 - 可达性分析
- 方法区回收
1.对象回收 - 引用计数
就是在对象中加上引用计数器,被引用了一次就加一,用完了就减一,这种方法在java中不会使用,因为在java中存在循环
引用的问题,如obja和objb相互引用,obja里面有个属性引用了objb对象,objb里面有个属性引用了obja对象,这样的
话引用计数是无法使用的。
2.对象回收 - 可达性分析
简单来说就是将对象及其引用关系看作一个图,选定活动的对象作为GCRoots;
然后跟踪引用链条,如果一个对象和GCRoots之间不可达,也就是不存在引用,那么即可认为是可以被回收的对象。
jvm先找出所有可以作为GCRoots的对象集合,然后通过GCRoot找出所有正在使用的对象进行标记,那些没有被标记为正在
使用的就会被垃圾搜集器回收。
3.方法区回收
这个在上次类加载器已经说过了,类被卸载需要满足以下两个条件,这就是方法区回收
①:该Class所有的实例都已经被GC销毁;
②:加载该类的所有ClassLoader实例都已经被GC;
关于可达性算法中提到的引用类型是有很多种的,下面我们来详细了解一下
4.引用类型
1. 强引用(StrongReference):
最常见的普通对象引用,只要还有强引用指向一个对象,就不会回收。
如Student s = new Student();强引用时只要这个s指向对象,那么这个对象就不会被回收
2. 软引用(SoftReference):
JVM认为内存不足时,才会去试图回收软引用指向的对象。
如Student s = new Student();弱引用时即使这个s指向对象,那么这个对象也有可能被回收
使用场景:
图片缓存。图片缓存框架中,“内存缓存”中的图片是以这种引用保存,使得JVM 在发生OOM之前,可以回收这部分缓存。
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null) {
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
} else {
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
3. 弱引用(WeakReference):
虽然是引用,但随时可能被回收掉,具体怎么回收要看jvm怎么定义。
使用场景:在下面的代码中,如果类B不是虚引用类A的话,执行main方法会出现内存泄漏的问题,因为类B依然依赖于A。
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B(a);
a = null;
System.gc();
System.out.println(b.getA()); // null
}
}
class A {}
class B {
WeakReference<A> weakReference;
public B(A a) {
weakReference = new WeakReference<>(a);
}
public A getA() {
return weakReference.get();
}
}
4. 虚引用(PhantomReference):
不能通过他访问对象。提供了对象被finalize以后,执行指定的机制(Cleaner)。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收
一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);
使用场景:
可以用来跟踪对象被垃圾回收的活动。一般可以通过虚引用达到回收一些非java内的一些资源比如堆外内存的行为。例如:
在DirectByteBuffer中,会创建一个PhantomReference的子类Cleaner的虚引用实例用来引用该DirectByteBuffer
实例Cleaner创建时会添加一个Runnable实例,当被引用的DirectByteBuffer对象不可达被垃圾回收时,将会执行
Cleaner实例内部的Runnable实例的run方法,用来回收堆外资源。
5.可达性级别
1. 强可达(Strongly Reachable):
一个对象可以有一个或者多个线程可以不通过各种引用访问到的情况。
2.软可达(Softly Reachable):
就是当我们只能通过软引用才能访问到对象的状态。
3.弱可达(Weakly Reachable):
只能通过弱引用访问时的状态。当弱引用被清除的时候,就符合销毁条件。
4. 幻象可达(Phantom Reachable):
不存在其他引用,并且finalize过了,只有幻象引用指向这个对象。
5.不可达(unreachable):
意味着对象可以被清除了。
6.小结
这里对可达性算法做一个小小的总结,所谓可达性算法就是找到一个能够用来作为入口对象,入口对象就是指我们可以通过这个对象顺藤摸瓜,找到所有正在被使用的对象并标记,这种对象一般存在常量池和虚拟机栈中,剩下未被标记的对象就是可以被回收的对象。
二.垃圾收集算法
1.标记-清除(Mark-Sweep)算法:首先标识出所有要回收的对象,然后进行清除。标记、清除过程效率有限,有内存碎片化问题,不适合特别大的堆;收集算法基本基于标记-清除的思路进行改进。
2.复制(Copying)算法:划分两块同等大小的区域,收集时将活着的对象复制到另一块区域。
拷贝过程中将对象顺序放置,就可以避免内存碎片化。复制+预留内存,有一定的浪费。
3.标记-整理(Mark-Compact):类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
三.分代收集
根据对象的存活周期,将内存区域划分为几个区域,不同的区域采用合适的垃圾收集算法。
新对象会分配到Eden,如果超过-XX:+PretenureSizeThreshold:设置大对象直接进入老年代的阈值。
新生代
新生代 S0:S1:Eden -> 1:1:8
新生代:老年代 -> 1:2
老年代
四.垃圾收集器
1.串行收集器- Serial GC -XX:+UseSerialGC
单个线程来执行所有垃圾收集工作,适合单处理器机器。
Client模式下JVM的默认选项
2.串行收集器- Serial Old -XX:+UseSerialOldGC|
可以在老年代使用,它采用了标记-整理(Mark-Compact) 算法,区别于新生代的复制算法
如下图:
灰色线程代表用户线程,棕色的线代表GC线程,原理是启动一个线程去做一些标记整理的事情,当GC线程工作的时候有一个很重要的特性叫stop-the-world,会停止所有用户线程的操作, GC线程工作完成后才能继续执行用户线程,所以问题就是不能让GC线程和用户线程同时进行。所以不适合使用在多核操作系统上面。
3.并行收集器- Parallel GC -XX:+UseParallelGC
4.并行收集器- Parallel Old GC -XX:+UseParallelOldGC
server模式JVM的默认GC选择,整体算法和Serial比较相似,区别是新生代和老年代GC都是并行进行;
可以设置GC时间或吞吐量等值,可以自动进行适应性调整Eden,Survivor大小和MaxTenuringThreshold的值。
也称为吞吐量优先的GC:吞吐量=用户代码运行时间/ (用户代码运行时间+ GC时间)
-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等。
-XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于0的整数。
-XX:GCTimeRatio:设置吞吐量大小,它的值是- -个0-100之间的整数。
-XX:+UseAdaptiveSizePolicy:打开自适应GC策略。以达到在堆大小、吞吐量和停顿时间之间的平衡点。
那么不管是串行收集器还是并行收集器,stop-the-world的停顿时间还是存在的, 在GC线程工作的时候用户线程是无法访问的,虽然这个时间很短,一般我们感觉不到。JVM团队也是在一直优化,后来提出了并发收集器。
5.并发收集器- CMS (Concurrent Mark Sweep) GC -XX:+UseConcMarkSweepGC
专用老年代,基于标记-清除(Mark-Sweep) 算法,设计目标是尽量减少停顿时间。
采用的标记-清除算法,存在着内存碎片化问题,长时间运行等情况下发生full GC(全局GC),导致恶劣的停顿。
CMS会占用更多CPU资源,并和用户线程争抢。
减少了停顿时间,这一点对于互联网web等对时间敏感的系统非常重要,一直到今天,仍然有很多系统使用CMSGC。
如下图:
CMSGC会有一个初始标记的过程,这一步仍然会有stop-the-world的现象,这一步是为了找到GCRoot,然后通过GCRoot找到有哪些对象正在被使用,接下来就会有一个并发标记的过程,就是多个GC线程和用户线程同时执行,这个过程GC线程会占用CPU资源并且会和用户线程进行争抢,所以并发的GC线程不能设置的太多,标记之后会进行一次重新标记, 最后就执行并发清除。
6.并行收集器- ParNew GC -XX:+UseParNewGC
新生代GC实现,它实际是Serial GC的多线程版本。
可以控制线程数量,参数: -XX:ParallelGCThreads
这里要提一下,并发收集器CMSGC和并行收集器ParNewGC是JVM的一些尝试,同时CMSGC在整个GC效果和算法的层面已经没有太多的可优化空间,所以orcale团队在JDK1.8已经明确不会在维护了,后期可能会剔除。
最常见的应用场景是配合老年代的CMS GC工作。参数-XX:+UseConcMarkSweepGC
7.并发收集器- G1 -XX:+UseG1GC
针对大堆内存设计的收集器,兼顾吞吐量和停顿时间,JDK9后为默认选型,目标是替代CMS;
G1将堆分成固定大小的区域,Region之间是复制算法,但整体上实际可看作是标记-整理(MarkCompact) 算法;
可以有效地避免内存碎片。红色新生代(Eden和Surivor),淡蓝色老年代。找不到大内存时执行FullGC。
整体布局图
整体GC阶段图
由于是新出来的,所以后续在看吧!
8.垃圾收集器组合
如图:现在一般使用的是以下三种
1.CMS + ParNew
2.Parallel Scavenge + Parallel Old(默认使用)
3.G1
五、总结
今天我们学习了垃圾回收器怎么去回收、标记一个垃圾的,怎么样进行垃圾收集的以及总共有哪些垃圾收集器,这些收集器的
区别又是什么,主要在于并行、串行、并发之间的区别,组合垃圾收集器他们之间又有什么区别,接下来我会在JVM调优学习
中也会为大家展示他们之间的区别,学无止尽,不积跬步,无以至千里,愿与君共勉!当然有问题可以随时私信我,希望大家
能够多多交流,一起学习一起进步!