在C/C++这样的语言中,分配和释放内存是一个手动过程,需要编程人员自己对内存进行管理,常常由于编程人员的失误,导致内存泄漏。在Java中,释放内存的过程由JVM帮我们处理,JVM通过垃圾收集器自动释放那些不再使用的内存。
自动垃圾收集
自动垃圾收集是查看堆内存,识别正在使用和未使用的对象以及哪些对象未被删除并删除未使用的对象的过程。使用中的对象意味着程序某些部分仍然有该对象的引用,当程序的任何部分都不再这些对象时,这些对象就成为了未使用对象,因此可以回收这些对象使用的内存。
如何确定需要回收的对象
引用计数
有一种被称为引用计数的判断方式,每个对象内部都有一个引用计数器,每次有新的引用指向这个对象时,引用计数器+1;去掉一个引用时,计数器-1。当计数器为0时,表明对象不再使用,可以被回收。引用计数法简单易用,但是会出现循环引用问题,如果有两个对象相互引用,并且没有其他的引用指向他们,这两个对象是无法访问的,他们不再被使用,但是引用计数不为0,无法被回收。
可达性分析
Java中使用的是可达性分析。简单来说,就是将对象的引用关系看作一个图,选定活动对象队伍GC Roots,然后跟踪引用链,如果一个对象从GC Roots不可达,就被认为是未使用对象,可以被回收。
可以作为GC Roots的对象:
- 虚拟机栈中正在引用的对象
- 本地方法栈中正在引用的对象
- 静态属性引用的对象
- 方法去常量引用的对象
引用类型与可达性级别
在Java中,有四种引用类型:
- 强引用(StrongReference):最常见的、普通的对象引用,只要还有一个强引用指向一个对象,该对象就不会被回收
- 软引用(SoftReference):当JVM认为内存不足时,才会去回收软引用指向的对象,常用于JVM缓存
- 弱引用(WeakReference):随时可能被回收,只要进行GC就会回收
- 虚引用(PhantomReference):不是真正的引用,不能通过它访问对象,供对象被finalize后,执行指定逻辑的机制
可达性级别有五种:
- 强可达:至少有一个强引用能访问的对象
- 软可达:只能通过软引用访问到的对象
- 弱可达:只能通过弱引用访问到的对象
- 幻象可达:不存在其他引用,并且已经finalize过,只有虚引用指向该对象
- 不可达:没有引用指向的对象,意味着可以回收
垃圾收集算法
- 标记-清除(Mark-Sweep)算法:首先标识处所有需要回收的对象,然后进行清除。缺点:标记-清除过程效率有限,有内存碎片化的问题,不适合特别大的堆;新的算法基本都是基于标记清除做出的改进
- 复制(Copying)算法:划分两块同等大小的区域,垃圾回收时,将存活的对象复制到另一块区域,然后清除本区域的所有对象。拷贝过程将对象顺序放置,避免了内存碎片化。缺点:同一时刻只能使用一半的内存,造成内存浪费
- 标记-整理(Mark-Compact)算法:类似于标记-清除,在清理过程中将对象移动,按顺序放置,确保移动后的对象占有连续内存空间,避免内存碎片化。缺点:多了移动对象的步骤,效率不高
分代收集
不同的垃圾收集算法各有优缺点,难以找到一种通用的垃圾回收算法,于是提出了一种分代收集的概念。根据对象的存活时间,将内存划分为几个区域,不同的区域使用不同的垃圾收集算法。
JVM将堆内存分为年轻代和老年代(Tenured),一般是1:2的大小,年轻代又分为两个Survivor(from、to)区和一个Eden区,一般为1:1:8。对象是在Eden区创建,年轻代采用的是复制算法,当Eden区内存不够时,会进行minor GC,将存活对象复制到空闲的Survivor区,清空Eden区和另一个Survivor区,并且对象经历垃圾回收次数+1,因此总会有一个Survivor是空闲的。
对象进入老年代的方式:
- 经历过一定次数(-XX:MaxTenuringThreshold,HotSpot默认15)minor GC,仍然存活的对象会被转移到老年代
- 当Survivor区内存不够时,也会将对象放入老年代
- 大对象(超过-XX:PretenureSizeThreshold)会直接在老年代创建
老年代的垃圾回收称为major GC,采用的是标记整理算法。
垃圾收集器
Serial GC与Serial Old
- 串行收集器Serial GC:-XX:+UseSerialGC,但个线程执行所有的垃圾收集工作,适合单核机器。是Client模式下默认选项
- 串行收集器Serial Old:-XX:+UseSerialOldGC,SerialGC的老年代版本,采用标记整理算法
Parallel Scavenge GC与Parallel Old GC
并行收集器Parallel Scavenge GC与Parallel Old GC:-XX:+UseParallelGC、-XX:+UseParallelOldGC,server模式JVM默认GC选择,整体算法与Serial相似,只是以多线程并行进行,可以设置GC时间吞吐量等值,可以自适应的调整Eden、Survivor大小和MaxTenuringThreshold和PretenureSizeThreshold值,是一种吞吐量优先的GC。
吞吐量=用户代码运行时间/(用户代码运行时间+GC时间)
- -XX:ParallelGCThreads:设置垃圾回收线程数。通常与CPU数量相等
- -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间
- -XX:GCTimeRatio:设置吞吐量大小,0-100的整数
- -XX:+UseAdaptiveSizePolicy:打开自适应GC策略,自定调整堆大小、吞吐量、停顿时间之间的平衡点
CMS GC
串行收集器和并行收集器在垃圾回收过程中会停止所有用户线程(stop-the-world),对于互联网web等对时间敏感的系统,有时无法满足需求。并发收集器CMS GC(-XX:+UseConcMarkSweepGC)专用于老年代,基于标记清除算法,设计的目标是尽量减少停顿时间。并发收集器的重要改进是垃圾回收和用户的业务线程并发执行,减少了stop-the-world时间。
CMS垃圾回收分为四个阶段,初始标记:此阶段需要stop-the-world,找出所有的GC Roots;并发标记:此阶段与用户线程一起执行,从GC Roots找出存活的对象;重新标记:最终的标记阶段,需要stop-the-world,对并发标记进行检查;并发清除:清除未存活的对象。
CMS采用并发的方式,减少了停顿时间,这一点对于互联网公司非常重要,直到今天,仍有许多公司在使用。虽然标记清除算法速度比较快,但是存在内存碎片化问题,长时间运行情况下会发生full GC,导致恶劣停顿,此外CMS和用户线程争抢,占用更多CPU资源
ParNewGC
ParNewGC(-XX:+UseParNewGC)也是并行收集器,一种新生代的GC,是SerialGC的多线程版本,可以控制线程数量(-XX:ParallelGCThreads)。最常见的是配合老年代的CMS一起使用
G1
G1(-XX:+UseG1GC)收集器是一种并发收集器,它是针对大堆内存设计的,兼顾吞吐量与停顿时间,目标是代替CMS,为JDK9之后的默认收集器。G1具有自己的分代方式,将堆分成多个固定大小的区域,Regin之间是复制算法,但也会以区域为对象进行整理,整体上实际可看作是标记整理算法,可以有效避免内存碎片。红色新生代(Eden和Survivor),淡蓝色老年代。
常见的垃圾收集器组合
![](https://i-blog.csdnimg.cn/blog_migrate/ce9ed5a0f909a9fdc50e94a3d59f729b.png)