Math.java 一个简单的类
public class Math {
public Math() {
}
public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
可以使用 javap -c Math.class > math.txt生成JAVA虚拟机执行的指令码
JVM常用指令集可以看到整个类的运行机制如下:
当一个对象创建时,一般会在Eden区(如果对象太大,大于Eden区的存储大小的50%,小于老年代的大小,触发担保机制,会直接放到老年代),当Eden区放满后,触发yangGC/minorGC,由字节码执行引擎启动垃圾收集线程进行垃圾回收。利用可达性分析算法,从GCRoot(线程栈的本地变量,静态变量、本地方法栈)开始执行查找,直到找到最终变量,标记此链条上的所有变量为非垃圾变量,并且把对象从Eden区挪到Survivor0区,并且分代对象年龄+1,如果一直存活直到分代年龄到达15后挪到老年代,其他剩余在年轻代的对象都认为是垃圾对象,需要回收。
老年代满了后会触发full GC,如果一直放不进老年代就会触发OutOfMemery
1.标记清除Mark-Sweep->产生较多内存碎片
2.拷贝(copying)->需要2倍的空间
3.标记压缩(Mark-Compact)->避免了内存碎片化的问题,但是效率上相对于标记-清除法略低
4.分代收集(Generational Collection) ->一般分成Young区和Old区分别来收集
Serial(Young区)---Serial Old (Old区) (-XX:+UseSerialGC)
第一代垃圾收集器, 单进程处理,在它进行垃圾收集时,必须暂停所有用户线程。
Serial(Young区)采用Copying算法;
Serial Old收集器采用Mark-Compact算法。
不适合多线程项目,效率低下
2.Parallel Scavenge(Young区)( -XX:+UseParallelGC)---PerallelOld (Old区)(-XX:+UseParallelOldGC)[JDK8默认的收集器]:
Parallel Scavenge:是Serial(串行)收集器的多线程版本,使用多个线程进行垃圾收集
PerallelOld: 使用多线程和Mark-Compact(标记-整理)算法
Paraller参数
-XX:SurvivorRatio Survivor区的比例
-XX:PreTenureSizeThreshold 大对象的阈值
-XX:ParallelGCThreads 并行收集器的线程数,一般设为和CPU核数相同
-XX:+UseAdaptiveSizePolicy 自动选择各区大小比例
3.ParNew(Young区)(XX:UserParNewGC)---CMS(Old区)(-XX:+UseConcMarkSweepGC):
ParNew:工作在Young区的的ParallerScavenge增强版,配合CMS使用。
CMS:是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep(标记-清除)算法。实现了GC线程和用户线程同时工作的能力,极大提高了JVM垃圾收集的效率
三色标记法是可达性分析算法中,垃圾收集器在并发标记阶段为了提高效率和解决一些漏标问题提出的一种扫描标记的实现手段,三色标记法将对象分为3类:黑色、灰色、白色。
黑色:标识对象已经被收集器访问过,并且该对象引用的对象都已经被扫描,黑色对象是安全存活的。
灰色:标识对象已经被收集器访问过,但是该对象引用的对象还未完全扫描完成。
白色:标识对象还未被扫描过,GC刚开始阶段所有对象都是白色,在可达分析之后如果对象仍然是白色,则会被当做垃圾回收。
CMS的缺点:CMS的重新标记阶段,必须重新扫描, 效率会低, STW时间会变长
CMS的相关核心参数:
-XX:+UseConcMarkSweepGC:启用cms
-XX:ConcGCThreads:并发的GC线程数
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW
4.G1(-XX:+UseG1GC)
物理不分代,逻辑分代,采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。
G1的常用参数
● -XX:+UseG1GC 使用G1收集器
● -XX:G1HeapRegionSize 指定Region的大小,1MB~32MB,必须是2的幂次方。
● -XX:MaxGCPauseMillis 指定最大的GC暂停时间。
● -XX:G1NewSizePercnet 指定新生代占比,默认是5%。
● -XX:G1MaxNewSizePercent 新生代内存最大空间
● -XX:InitiatingHeapOccupancyPercent 指定老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC)
● -XX:GCTimeRatio GC时间建议比例,G1会根据这个值调整堆空间
适用场景
● G1适合>8G堆内存的JVM,<8G有时候反而不如CMS+ParNew组合,因为复杂的底层。
● G1适合对象分配和晋升速度快的场景。
● G1适合50%以上的堆被存活对象占用的场景
● G1适合对STW要求高的场景。
一次回收会将会触发所有年轻代的回收
5.ZGC(zero GC)
分页算法,算法是颜色指针
也是分成一个一个小区域,不再分代,默认100毫秒触发一次回收
6.Shenandoah:
RedHat 提供的,和ZGC类似
7.Epsilon:什么都不做,主要是为了JVM开发人员对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:+PrintGCTimesStamps
-XX:+PrintGCApplicationConcurrentTime 打印应用程序时间
-XX:+PrintGCApplicationStoppedTime 打印暂停时长
-XX:+PrintReferenceGC 记录回收了多少种不同引用类型的引用
-verbose:class 类加载详细过程
-XX:+PrintVMOption
-XX:+PrintFlagsFinal -XX:+PrintFlagsInitial
-Xloggc:opt/log/gc.log
-XX:MaxTenuringThreshold 升代年龄,最大值15
-XX:PreBlockSpin 锁自旋次数
多数的 Java 应用不需要在服务器上进行 GC 优化; 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); 减少创建对象的数量; 减少使用全局变量和大对象; GC 优化是到最后不得已才采用的手段; 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。
调优:从业务场景开始
无监控(压力测试,能看到结果),不调优
步骤:
熟悉业务场景(没有最好的垃圾收集器,只有最合适的垃圾回收器)
响应时间、停顿时间[CMS G1 ZGC](需要给用户作响应)
吞吐量 = 用户代码运行时间/(用户代码运行时间+GC时间)
选择回收器
使用 java -XX:+PrintCommendLineFlags --version
查看垃圾收集器用的哪个
计算内存需求(经验值1.5G 16G)
选定CPU(越高越好)
设定年代大小、升级年龄
设定日志参数
-Xloggc:/opt/xxx/logs/xxx-xx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
观察日志情况
命令行:
top 命令观察到问题:内存不断增长 CPU占用率居高不下
top -HP 观察进程中的线程,哪个线程CPU和内存占比高
jps定位具体java进程
jstack 定位线程状况,重点关注:WAITING BLOCKED
例如:waiting on <0x0000000088ca3310>( a java.lang.Object),假如有一个进程中100个线程,很多线程都在waiting on
jinfo pid
jstat -gc 动态观察gc情况/阅读GC日志发现频繁GC/arhas观察/jconsole/jvisualVM
jstat -gc 4655 500 每500毫秒打印GC情况
jmap -histo 进程号 | head -20 查找有多少对象产生
jmap -dump:format=b, file=xxx.hprof pid
线上系统内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)
设定了参数HeapDump, OOM的时候会自动产生对存储文件(不是很专业,因为监控,内存增长会报警)
很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
在测试环境中压测(产生类似内存增长问题,在堆还不是很大的时候进行转储)
java -Xms20M -Xmx:20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError 类路径
使用MAT/jhat/jvisualvm进行dump文件分析
jhat -j-mx512M xxx.dump IP
工具:
使用工具定位问题
1.JAVA VisualVM
2.Arthas(阿里开源)推荐使用
使用方法
1.登录主机 将arthas-boot.jar放到主机目录下,
2.使用命令:java -jar arthas-boot.jar 启动arthas
3.输入dashboard命令,显示出JVM大盘信息
4.看到大盘上
有个线程一直占用CPU很高,ID为8,通过thread 8定位到代码行
有2个线程被阻塞了,可以通过thread -b 查看定位代码问题
提供反编译命令 jad com.ArthasTest 查看文件
ognl命令提供了修改内存的值
调优:
JVM调优目的是为了减少 Stop The World(停掉用户进程专门去做GC)现象,例如频繁fullGC
为何要 Stop The World?
因为如果用户进程在操作的过程中进行GC,会存在用户线程结束后对象没办法回收的情况,即对象是否是垃圾还是非垃圾会发生变化,无法处理,因此JVM是把用户进程停止后专门进行垃圾回收
系统压力不发生变化情况下,能否对JVM调优,使其几乎不发生fullGC?
朝生夕死的对象尽量放在年轻代,可以调整老年代和年轻代的比例, 评估后设置合理的比例
策略 1:将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
策略 2:大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。-XX:PretenureSizeThreshold 可以设置直接进入老年代的对象大小。
策略 3:合理设置进入老年代对象的年龄,-XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。
策略 4:设置稳定的堆大小,堆大小设置有两个参数:-Xms 初始化堆大小,-Xmx 最大堆大小。
策略5:注意: 如果满足下面的指标,则一般不需要进行 GC 优化:
MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。