1. 如何定位垃圾
垃圾就是没有引用的对象,那么如何确定是否还存在引用呢
- 引用计数(ReferenceCount):存在循环引用问题
- 根可达算法(RootSearching):从跟开始寻找(线程栈变量,静态变量,常量池,JNI指针),找不到的就算垃圾
2. 垃圾回收算法
标记清除(mark sweep) - 位置不连续 产生碎片 效率偏低(两遍扫描)
拷贝算法 (copying) - 没有碎片,浪费空间
标记压缩(mark compact) - 没有碎片,效率偏低(两遍扫描,指针需要调整)
3. JVM中的垃圾回收器
10种如下,JVM回收内存通常是组合使用,不同的垃圾回收器采用不同的策略
左边6个分代模型中,下面的是老年代,上面的是年轻代,一般是如图所示搭配(但也可以混杂搭配,看其他虚线)
1.8默认是PS+PO(新生代Parallel Scavenge,老年代Parallel Old),但是可以用分区模型G1(推荐,有些公司运维有JVM调优经验的话说不定会用这个)
下面介绍一下各个组合
3.1 分代模型-Serial系列
如图解释的很清楚,当垃圾回收线程来了,工作线程STW,垃圾回收线程完了,工作线程才能继续
STW:Stop The World,如字面意思,马上!!停止!!!
为什么有时候机器会突然卡顿一下,就是因为STW了
3.2 分代模型-Parallel系列
随着内存越来越大,Serial有点慢,于是出现了Parallel
和Serial差不多也是STW,不过Parallel嘛,垃圾回收线程是多个且并行的
3.3 分代模型-CMS系列
还是随着内存的越来越大,Parallel很慢,原因:线程数和效率不是线性提升的,因为线程需要上下文切换ContextSwitch,到达一定数量后,线程多反而耗费了大量时间在此
CMS:concurrent mark sweep
Parnew和Parallel Scavenge基本一样,只是为了配合CMS而产生的
CMS过程如下:
初始标记也是STW,但是只GCRoot扫描根(前面说的根可达算法可还记得?),很少所以时间可以接受
重新标记也是STW,这里有个bug,后面讲
CMS三色标记法和错标问题
并发标记存在问题,就是错标
情况1:在标记的时候突然有个对象引用没了(成为了垃圾,称为浮动垃圾),这种就是漏标了,这种问题不大,大不了下次gc再清理就是了
情况2:一个对象本来是垃圾,突然又有引用了(存在这种情况嘛?有的,缓存),这种错标就比较严重了
所以存在重新标记的过程
三色标记法
黑色:对象标记,成员field也被标记
灰色:对象标记,成员还未标记
白色:没有遍历到的字段
标记是怎么标记的呢?CMS标记采用三色标记法,重新标记阶段扫描就是扫描灰色的对象。说回情况2,如果第一次标记后黑色的对象有个指针域指向了某个白色对象,再次标记的时候不会再扫描黑色对象,此时就产生了错标
解决办法
- CMS解决办法:incremental update:简单的说就是A引用变了的时候重新将A置为灰色,这样就可以重新扫描到了
这种办法存在一个bug,就是说对象A有两个field,多线程的时候垃圾回收线程标记了field1不是垃圾,然后扫描field2,工作线程使得field1重新指向了一个垃圾对象D,这样本来A应该被标记为灰色,但是垃圾回收线程不知道filed1改变了,标记完field2后将A置为黑色
因此才需要remark过程,但是这个过程导致CMS这个原本号称“解决STW”的算法产生了历史上最长的STW,也就是因为这样,CMS并没有作为任何一个JDK版本的默认垃圾回收器
- G1解决办法:SATB
3.4 JVM的堆内存模型
JVM堆的具体内存分区如下,图中的数字是大小比例
可以看到新生代用的拷贝算法,但不是一般的拷贝,是eden:survivor1:survivor2=8:1:1,第一次gc从eden到s1,然后在s1和s2之间来回跳,到达一定的次数后进入老年代(这个次数默认15,CMS默认6,可以通过-XX:MaxTenuringThreshold配置)
4. JVM调优实战经验
服务器升级加大了内存,反而更加卡顿。原因是内存越大,FGC时间越长。解决办法:PS -> PN + CMS 或者 G1
线上CPU突然100%。那么一定有线程在占用系统资源,
- 找出哪个进程cpu高(top)
- 该进程中的哪个线程cpu高(top -Hp)
- 导出该线程的堆栈 (jstack)
- 查找哪个方法(栈帧)消耗时间 (jstack)
- 工作线程占比高 | 垃圾回收线程占比高
系统内存飙高,如何查找问题?(面试高频)
- 导出堆内存 (jmap)
- 分析 (jhat jvisualvm mat jprofiler … )
如何监控JVM
- jstat jvisualvm jprofiler arthas top…
附录
1. 对象分配过程
- 一些基本类型的变量是有机会分配在栈上的比如局部变量,怎么判定是否能分配在栈上?如果逃逸分析没有逃逸,标量替换可以替换,则分配在栈上
- 逃逸分析:看附录2
- 标量替换:即一个对象可以用两个基本类型的变量来替换它,就称为可以标量替换,比如一个对象只有两个int的类型,那么我们完全可以用两个int变量代替这个类(结构体)
- 如果对象太大,直接老年代,否则会先TLAB(ThreadLocalAllocationBuffer线程本地分配缓冲区),这个细节一般不会有人问起,但是确实存在的,Eden区有一部分TLAB,线程争用后才进入Eden区的公共空间
2. 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化:
- 栈上分配
Java虚拟机中,如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力将会小很多。
- 同步消除
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除。
- 标量替换
标量是指一个数据已经无法再分解成更小的数据来表示了,Java虚拟机的原始数据类型都不能再进一步分解,它们就可以称为标量。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。除了可以让对象的成员变量在栈上(栈上存储的数据,有很大的概率会被虚拟机分配到物理机器高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。
3. JVM常用命令
3.1 监控相关
基础命令
- jps:查询运行中的java进程编号和名称,相当于ps命令,但只有java相关的
- jinfo pid:作用不大,可以查看某个进程的jdk信息,比如jdk版本啊classpath啊encoding啊当前的vm参数啊等等
- jstack pid:java堆栈信息,类似于idea里面的debug控制台左边,显示了某个进程的所有线程运行了哪个方法,可以用来查死锁
- jmap -histo pid:查看某个java进程内存分配状况
- jstat:可以观察到classloader,compiler,gc相关信息,实时监控资源和性能
工具(arthas)
上面介绍的基本命令基本上不用,因为通常都用工具,就很像git,一般都用sourcetree或者idea而不是纯命令行,因为工具更加强大,所以我们来看看
gui工具:jconsole, jvisualvm这些可视化工具都很不错,而且是jdk自带的,但是只能在本地开发压测和debug用,线上服务器通常没有gui,所以用的更多的是arthas(阿里开源的工具,下载并启动jar包,java -jar arthas-boot.jar
,在黑框上模拟图形化界面)
下面介绍arthas一些命令
- help:帮助文档
- dashboard:仪表盘模拟图形界面,很强,可以监控CPU和内存,相当于top,看图
jvm:相当于jinfo,显示了jvm相关信息,垃圾回收器等
thread [线程id]:相当于jstack,只列出了线程列表,要看某个线程的详情可以thread 线程id,要找死锁,直接thread -b可以看到某个死锁阻塞状况
heapdump:将内存占用信息导出到具体的文件夹,用于查oom,如果疯狂fgc,或者已经出现了oom,那么可以heapdump导出到具体的文件夹,然后用前面说的gui工具打开进行本地分析,相当于jmap命令
注意:arthas的headdump和jmap在生产环境中都不太能用,因为内存有可能很大,完整dump备份一次要很久,生产环境通常使用XX:+HeapDumpOnOutOfMemoryError,代表在oom之前自动导出
3.2 GC常用参数
- -Xmn -Xms -Xmx -Xss
年轻代 最小堆 最大堆 栈空间 - -XX:+UseTLAB
使用TLAB,默认打开 - -XX:+PrintTLAB
打印TLAB的使用情况 - -XX:TLABSize
设置TLAB大小 - -XX:+DisableExplictGC
System.gc()不管用 ,FGC - -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintHeapAtGC
- -XX:+PrintGCTimeStamps
- -XX:+PrintGCApplicationConcurrentTime (低)
打印应用程序时间 - -XX:+PrintGCApplicationStoppedTime (低)
打印暂停时长 - -XX:+PrintReferenceGC (重要性低)
记录回收了多少种不同引用类型的引用 - -verbose:class
类加载详细过程 - -XX:+PrintVMOptions
- -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial
必须会用 - -Xloggc:opt/log/gc.log
- -XX:MaxTenuringThreshold
升代年龄,最大值15 - 锁自旋次数 -XX:PreBlockSpin 热点代码检测参数-XX:CompileThreshold 逃逸分析 标量替换 …
这些不建议设置
Parallel常用参数
- -XX:SurvivorRatio
- -XX:PreTenureSizeThreshold
大对象到底多大 - -XX:MaxTenuringThreshold
- -XX:+ParallelGCThreads
并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同 - -XX:+UseAdaptiveSizePolicy
自动选择各区大小比例
CMS常用参数
- -XX:+UseConcMarkSweepGC
- -XX:ParallelCMSThreads
CMS线程数量 - -XX:CMSInitiatingOccupancyFraction
使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收) - -XX:+UseCMSCompactAtFullCollection
在FGC时进行压缩 - -XX:CMSFullGCsBeforeCompaction
多少次FGC之后进行压缩 - -XX:+CMSClassUnloadingEnabled
- -XX:CMSInitiatingPermOccupancyFraction
达到什么比例时进行Perm回收 - GCTimeRatio
设置GC时间占用程序运行时间的百分比 - -XX:MaxGCPauseMillis
停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
G1常用参数
- -XX:+UseG1GC
- -XX:MaxGCPauseMillis
建议值,G1会尝试调整Young区的块数来达到这个值 - -XX:GCPauseIntervalMillis
?GC的间隔时间 - -XX:+G1HeapRegionSize
分区大小,建议逐渐增大该值,1 2 4 8 16 32。
随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
ZGC做了改进(动态区块大小) - G1NewSizePercent
新生代最小比例,默认为5% - G1MaxNewSizePercent
新生代最大比例,默认为60% - GCTimeRatio
GC时间建议比例,G1会根据这个值调整堆空间 - ConcGCThreads
线程数量 - InitiatingHeapOccupancyPercent
启动G1的堆空间占用比例