目录
背景
Java作为一门被广泛使用的面向对象语言,提供了诸如跨平台、丰富的类库、完善的生态等非常多优点,JVM内存分配与回收机制就是其中非常重要一项的特性。在JAVA语言编程中,开发人员并不需要像C语言一样,需要对内存进行手工分配与回收,内存的分配与回收完全是由JVM自动完成的,对开发人员来说,是完全透明的。JVM的这种处理方式带来了非常多的好处,比如减少开发人员的工作量、降低了像C语言中因丢失指针导致的内存泄漏等问题风险。
既然JVM能够自动完成内存的分配与回收,为什么还需要手工进行GC调优呢?
JVM自动垃圾回收机制更多的是基于应用所处的硬件及软件环境选择的默认的收集策略来进行的,在选择这些默认配置的时候,并无法感知应用的具体类型,究竟是停顿时间优先的(web交互式应用),还是吞吐量优化(任务分析型)的应用,这种默认策略在一般的应用场景下能够比较好的达到要求,但是在一些对性能要求比较严苛的场景下,往往比较难以到达要求,这时就需要人工介入进行GC调优。
内存布局
Java虚拟机运行时数据区
如Java虚拟机运行时数据区图所示,Java虚拟机运行时数据区有5个部分组成,分别为方法区、堆、虚拟机栈、本地方法栈、程序计数器,其中方法区与堆是由所有线程共享的,而虚拟机栈、本地方法栈、程序按计算器则是由单个线程私有的。
具体各个部分的作用如下:
- 方法区
-
方法区主要用于存储类对象、静态变量、常量等数据,是所有线程共享的数据区域。
- 堆
-
堆主要存放创建的对象、数组、线程对象等数据,是所有线程共享的数据区域,该区域也是垃圾回收的主要区域。
- 虚拟机栈
-
虚拟机栈用于方法代码中的局部变量、方法返回地址等信息,线程私有,随线程一起自动销毁,无需垃圾回收。
- 本地方法栈
-
本地方法栈用于存放本地方法执行过程中使用到的局部变量等数据,线程私有。
- 程序计数器
-
存放当前线程执行的地址信息,线程私有。
如何判断对象已死?
java虚拟机是如何定义垃圾的,如何判断一个对象是否存活?
- 引用计数法
定义:为每一个对象维护一个引用计算器,对象每次被引用则计数器加1,每次被释放,则计数器减1,当对象的计数器为0时,该对象将不再被其他任何对象引用,此时可判别该对象已死,可以被作为垃圾回收。
优点:实现简单
缺点:无法处理对象之间循环因引用的情况
- 可达性分析法
定义:java虚拟机维护一组称为GC Root对象集合(如:本地方法栈、静态变量、常量、线程池等对象),通过遍历GC Root集合,从GC Root对象开始,按照引用关系持续往下找,从而形成从GC Root开始的引用关系链,如果某一对象存在任何一条链中,则该对象目前是存活状态。反之,对象不在任何引用链中,则该对象已死,可以被回收。
优点:解决了引用计算法中循环引用无法回收问题。
缺点:实现起来比较复杂。
目前Java虚拟机采用 可达性分析法。
垃圾收集法算
- 标记清理
定义:
整个垃圾收集算法分2个阶段,分别为标记阶段、清理阶段。
标记阶段:主要目的是采用可达性分析算法,遍历GC Root集合,找出已经死亡的对象,并进行标记;
清理阶段:标记阶段找到的死亡对象逐个清理。
优点:实现简单。
缺点:因为需要遍历整个数据区的对象,性能低、产生内存碎片。
适用场景:内存小、运行时间短,适合客户端场景。
- 复制算法
定义:
复制算法将内存分为大小相等的两份,同一时间只使用其中一份,另一份用于在垃圾收集中存放存活的对象、用于备份,2分内存区交替使用。整个垃圾收集算法分3个阶段,分别为标记阶段、复制阶段、清理阶段。
标记阶段:主要目的是采用可达性分析算法,遍历GC Root集合,找出已经死亡的对象,并进行标记;
复制阶段:将标记阶段找到的存活对象复制到另一份未使用的内存区。
清理阶段:将原始的数据区整个清理掉,采用新的内存区分配对象。
优点:因为是整个内存区一次清理,所以高效;同时不会产生内存碎片。
缺点:需要浪费一半内存做担保,内存使用率只要50%,存在浪费。
适用场景:适用对象生命周期比较短(比如朝生夕死),垃圾收集效率高的场景;不适用对象生命周期长的情况,因为大量存活对象的复制,性能差。分代收集算中复制算法一般用对新生代数据区的垃圾回收。
- 标记整理
定义:
标记整理可以理解为标记清理算法的改良版,整个垃圾收集算法分2个阶段,分别为标记阶段、整理阶段。
标记阶段:主要目的是采用可达性分析算法,遍历GC Root集合,找出已经死亡的对象,并进行标记;
整理阶段:将标记阶段找到的存活对象都移动到内存区的一边,最后清理掉存活对象边界后的所有内存区域。
优点:性能相比标记清理有所提高,同时不会产生内存碎片。
缺点:生命周期比较短,存活率低的情况不适用
适用场景:对象生命周期长,存活率高,一般用在分代收集算法中对年老代对象的回收。
- 分代收集
定义:
分代收集算法将内存区分为新生代(或年轻代)、年老代 2个区域。新生代 又分为 Eden、2个surivior区,存放的对象生命周期比较短、垃圾回收的效率很高,该区域一般采用复制算法进行回收。年老代的对象存放的对象生命周期长。
分代收集算法中会为每一个对象维护一个年龄,新生代中的对象每经历一次垃圾回收,任然能存活,则该对象年龄加1,当对象的年龄达到某个阈值,将进入到年老代中,该阈值由java虚拟机参数-XX:MaxTenuringThreshold控制,默认15。
垃圾收集器
垃圾收集算法是方法论,那么垃圾器则是垃圾收集算法的工程实践。
垃圾收集中并行与并发?
并行:充分利用多核处理器资源,同时启动多个垃圾收集线程进行垃圾回收,在进行垃圾回收时,从而提高垃圾收集的效率,用户线程则处在挂起状态;
并发:垃圾收集线程和用户线程同时运行,降低应用停顿时间。
常用的垃圾收集器有:
- Serial
作用区域:年轻代
收集算法:复制算法
垃圾收集线程数:单线程
优点:实现简单
缺点:垃圾收集器时会发生STW,单线程性能低
适用场景:单核CPU、内存小,嵌入式设备或客户端;不适合多核CPU、大内存的环境,不然停顿时间会过长
- ParNew
定义:Serial收集器的多线程版本。可以通过-XX:UseParNewGC选项强制开启
作用区域:年轻代
收集算法:复制算法
垃圾收集线程数:多线程,默认和CPU数量一样,线程数量可以通过-XX:ParallelGCThreads自定义
优点:性能比Serial好,停顿时间变短;可以与CMS收集器结合使用;
缺点:不适合单核CPU的场景
适用场景:适合多核CPU、大内存、低停顿时间场景;
- Parallel Scavenge
定义:这是一款吞吐量优先的垃圾收集器,目标是达到一个可控制的吞吐量。(吞吐量:CPU执行用户代码上的时间与总时间的比值,总时间=CPU用户代码时间 + 垃圾收集时间)。
作用区域:年轻代
收集算法:复制算法
垃圾收集线程数:多线程,默认和CPU数量一样,线程数量可以通过-XX:ParallelGCThreads自定义
优点:可以实现一个可控的吞吐量
缺点:不适合单核CPU的场景
适用场景:适合多核CPU、大内存、低停顿时间场景;
参数:-XX:MaxGCPauseMillis参数控制最大停顿时间,大于0的值,以毫秒为单位;
-XX:GCTimeRatio参数控制吞吐量,大于0且小于100的整数,GCTimeRatio = 执行用户代码的CPU时间/花在垃圾收集上的时间,例如-XX:GCTimeRatio = 19,则吞吐量 = 19 / (19 + 1) = 0.95及吞吐量为95%,则花在垃圾收集上的时间为5%。-XX:MaxGCPauseMillis 与 -XX:GCTimeRatio 参数是相互矛盾的,更低的停顿时间,则需要更频繁的垃圾回收,则相应的吞吐量会降低。
- Serial Old
定义:Serial收集器的老年代版本。
作用区域:老年代
收集算法:标记-整理
垃圾收集线程数:单线程
优点:简单
缺点:性能低,停顿时间长
适用场景:适合单核CPU、小内存,如嵌入式设备或客户端应用
- Parallel Old
定义:Parallel Scavenge 收集器的老年代版本。吞吐量优先
作用区域:老年代
收集算法:标记-整理
垃圾收集线程数:多线程
优点:吞吐量优先,可以和Parallel Scavenge 收集器搭配使用
缺点:不适合单线程
适用场景:适合多核CPU、大内存
- CMS
定义:并发标记清理(Concurrent Mark Sweep),垃圾收集线程与用户线程一起运行,适合交互式低停顿应用,如Web
作用区域:老年代
收集算法:标记-清理
垃圾收集线程数:多线程
优点:垃圾收集线程与用户线程一起并发运行,低停顿
缺点:不适合单线程,产生内存碎片、产生浮动垃圾
适用场景:适合多核CPU、大内存
流程:
-
1、初始标记(STW);
-
2、并发标记
-
3、重新标记(STW)
-
4、并发清除
- G1
定义:Garbage-First ,目标实现一个可预测的低停顿时间
作用区域:年轻代、老年代
收集算法:分代收集、标记-整理、复制
垃圾收集线程数:多线程
优点:垃圾收集线程与用户线程一起并发运行,低停顿
缺点:不适合单线程,产生浮动垃圾
适用场景:适合多核CPU、大内存(大于6G)
流程:
-
1、初始标记(STW);
-
2、并发标记
-
3、最终标记(STW)
-
4、筛选回收
如何选择垃圾收集器?
GC性能指标
调优工具
参数
实践