JVM调优预备知识:Java GC算法介绍和垃圾回收器

序章:理解栈和堆

第一印象

栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。

为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,数据共享角度堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,存储能力栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把
对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。

1.JVM调优是做什么?

减少GC次数,提高服务器性能和稳定性,调优主要调整的JVM参数。
jvm参数有三种不同类型:
a、以-开头,标准JVM参数,各个版本都会支持
b、以-X开头, 非标准参数
c、以-XX开头

JVM常见的调优参数包括:
-Xmx:指定java程序的最大堆内存, 使用java -Xmx5000M -version判断当前系统能分配的最大堆内存;
-Xms:指定最小堆内存, 通常设置成跟最大堆内存一样,减少GC;
-Xmn:设置年轻代大小。整个堆大小=年轻代大小+年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8;
-Xss:指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误(StackOverflowError);
-XX:PermSize:指定方法区(永久区)的初始值,默认是物理内存的1/64,在Java8永久区移除, 代之的是元数据区,由-XX:MetaspaceSize指定;
-XX:MaxPermSize:指定方法区的最大值, 默认是物理内存的1/4,在java8中由-XX:MaxMetaspaceSize指定元数据区的大小;
-XX:NewRatio=n:年老代与年轻代的比值,-XX:NewRatio=2, 表示年老代与年轻代的比值为2:1;
-XX:SurvivorRatio=n:Eden区与Survivor区的大小比值,
-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1,因为Survivor区有两个(from, to)。
-XX:MaxTenuringThreshold surrivor区GC次数,设置为0,直接进入年老代,默认是15

收集器设置:
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息:
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置:
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置:
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


JVM实质上分为三大块,年轻代(YoungGen),年老代(Old Memory),及持久代(Perm,在Java8中被取消)。

2.GC算法

简单了解了JVM调优需要做什么之后,我们需要了解到JVM内存回收算法。在此之前,首先介绍下,JVM是如何判断对象已死:

2.1JVM如何确定对象要被回收?

a、引用计数算法:
对象有被引用就加1,引用失效,就减1,任何计数器为0 的对象都不会被使用了,
该算法简单、高效,但是有一个严重的问题,就是无法解决循环引用的问题。
b、可达性分析算法:
该算法就是通过一系列GC roots对象作为起点,从该节点往下搜索,搜索路径称之
引用链,当一个对象到GC roots没有任何引用链时就表示该对象到GC roots是不可达,
即表明该对象是不可用的,可被回收的。如下图所示:

在这里插入图片描述
补充说明:

1.不可达对象,并非是非死不可,当对象处于不可达状态时,这时候对象暂时处于”缓刑“阶段,
真正宣告一个对象死亡时,至少经历两次标记:如果对象在可达性分析师没有和GC Roots
有任何的应用链,将会被第一次标记并且进行一次筛选,筛选的条件是该对象是否有必要执行
finalize()。当对象没有覆盖finalize(),或者虚拟机已经调用过该方法,虚拟机将这两
种情况视为"没有必要执行"。**也就是说对象在回收前有且仅有一次机会,进行自我救赎!**
2.GC Roots有:
   a、虚拟机栈中引用的对象
   b、方法区中类静态属性引用的对象
   c、方法区中常量引用的对象
   d、本地方法栈中引用的对象(Native())

2.2 回收算法

1、标记-清除算法: 基础算法
该算法分为两个部分,标记和清除,
标记是通过可达性算法进行的,然后对标记完成的对象进行统一的回收。
缺点:一效率不高,二产生大量的碎片,当有大对象到来时,会找不到连续空间而触发一次GC

在这里插入图片描述

2.复制算法: 年轻代回收算法
为了解决效率问题,提出复制算法,就如年轻代中分为Eden和Survivor(可分为from和to两个区,按照1:1),
按照8:2比例分区;可通过JVM参数-XX:SurvivorRatio=n设置;默认是n=8
3、标记-整理算法: 老年代回收算法
复制算法对于存活率较低时,效率比较高,比如说老年代的对象大对数是不会死的,如果采用复制机会全部得复制,
效率自然低下,因此需要标记-整理,该算法的第一个过程和和标记-清除算法一样,后续步骤是存货对象向一端移动,然后清除端边界以外的内存,如图所示:

在这里插入图片描述
4、分代收集算法:
将内存划分为多块,如年轻代和年老代,根据各个年代的特点采用适合的算法。年轻代,对象存活率低,可使用赋值算法;年老代对象存活率很高,使用标记-整理算法!

3.垃圾收集器介绍

3.1对象从创建->进入内存->消亡的一生图解:

1.首先进入栈,内存够了就在这路呆着不走了,好处,当方法执行结束,清空栈内存,即直接弹出回收
2.TALB :TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
3.O:表示老年代
4.E:表示Eden区
5.S1和S2表示Survivor的两个区域
6.AGE:年龄,也就是来回复制的次数。

在这里插入图片描述

垃圾回收器的枚举:看下图

分类:分年轻代和年老代的收集器还有不分代的收集器;
组合:只有分代收集器有组合,理论上可以顺便组合,但有最优的组合就三种:Serial-Serial Old;ParNew-CMS;Parallel Scavenge-Parallel Old(JDK1.8 more的收集器简称PSPO)

在这里插入图片描述

3.2Serial:

在JDK1.3之前唯一的年轻代回收器,已经不采用这个收集器了,因为是单线程,这意味着必须停止所有的工作进程,直到它收集结束。Stop the World,听起来很酷,实则是恐怖了。收集过程如下图所示:
在这里插入图片描述

Serial Old

Serial Old 是Serial收集器的老年代版本,同样是一个单线程的收集器,使用标记-整理算法。

3.3ParNew收集器
其实就是Serial的的多线程版本,回收过程如下图所示:
多个垃圾回收线程并行处理:
在这里插入图片描述

3.4Parallel Scavenge:

也是使用复制算法的新生代收集器,但他是并行的多线程收集器。也被陈之为吞吐量优先收集器
它设计的目标是达到一个可控的吞吐量,吞吐量值得型CPU运行代码的时间和CPU总的消耗时间。
Parallel Scavenge提供两个控制吞吐量的参数:
1. -XX:MaxGcPauseMills:n,可设置一个大于0毫秒数,收集器尽可能保证GC在设定值范围内;
2. -XX:GCTimeRatio:n,设置吞吐量大小;

还有一个重要的开关参数:
--XX:UseAdaptiveSizePolicy
当这个参数打开之后就无需手工指定新生代大小、eden和suriviro的比例、进入
老年代年龄等细节参数了,这中调节被称之为GC自适应调节策略.

Parallel Old

是Parallel Scavenge老年代的版本,使用多线程的标记整理算法。
在这里插入图片描述

3.5 CMS收集器

该收集器的目标是:获取最短回收停顿的收集器。CMS是个并发的收集器。
cms收集器的四步骤:
在这里插入图片描述
并发标记会造成两个问题:漏标和错标。
漏标,再来一次就好了
错标:怎么搞?重新标记!三色算法,若有新的成员将他标记为灰色。

重新标记使用了三色标记算法:
白色:未被标到,即使垃圾,待回收
灰色:自身被标记,成员变量未被标记
黑子:自身和成员变量都被标记了。

cms收集器的缺陷:
1.浮动垃圾难以处理;
2.并发标记容易造成漏标
在这里插入图片描述

3.6G1收集器

G1是面向服务端应用的垃圾回收器,能够充分利用多CPU,多核环境的硬件优势,来缩短STW(stop the world)时间,并且能够通过并发来在回收垃圾时,运行java程序。
分代概念保留,逻辑分代,内存不分代。
G1回收垃圾能够提供规整的内存区域,因为从整体看书标记整理看,局部看却是复制算法,因此不会产生空间碎片。
G1回收期流程:四步骤
在这里插入图片描述

4.理解GC日志

eg:
在这里插入图片描述
在这里插入图片描述

1.日志开头Full GC 和GC表示此次垃圾回收停顿的类型有Full表名有STW出现;
2.33.125和100.667代表了GC发生的时间,表示虚拟机启动以来经过的秒数。
3.ParNew,DefNew表示GC发生的区域,这与GC收集器密切相关。
4.3324K->152K(3712K)含义是"GC前该内存区域已使用容量->GC后该内存使用的容量(该内存区域的总能量)”
5.0.0025925 secs表明该内存GC占用时间,单位是秒。

调优方向总结:

年轻代大小选择:
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择:
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。
最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息
持久代并发收集次数
传统GC信息
花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

参考:

本文主要参考书籍《java虚拟机第二版》,有感兴趣或者需要的小伙伴可以私聊我,发送pdf版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值