1、JVM的内存结构
(1)运行时数据区(规范)
——程序计数器(Program Counter register):还一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。它是线程私有的。
JVM 支持多线程同时执行,每一个线程都有自己的PC Register,线程正在执行的方法叫做当前方法,如果是java代码,PC Register里面存放的就是当前正在执行的指令的地址,如果是C代码,则为空
——虚拟机栈(Virtual Machine Stacks):也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
——本地方法栈(Native Method Stack):与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
——堆(Heap):是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动是创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可,就像我们的磁盘空间一样。
——方法区(Method Area):与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-heap(非堆),目的是与Java堆区分开来。
在JDK8里是MetaSpace,在JDK8之前是PermSpace
——运行时常量池(Runtime Constant Pool):是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
(2)JVM的内存结构
整个JVM的内存结构可以分为堆区和非堆区两大块,其中堆区又分为两大块:Young区和Old区;Young区又分为Survivor区和Eden区;Survivor区是分为两个同样大小的部分,一个是S0(From Survivor),另一个是S1(To Survivor),在同一个时间点上S0和S1只有一个是有值的,另一个是空的。非堆区在JDK8里有一个新的名字MetaSpace,这部分又包含压缩类空间(CCS)和CodeCache(JIT编译的Native代码)
MetaSpace | Class、Package、Method、Field、字节码、常量池、符号引用 |
CCS | 32位指针的Class |
CodeCache | JIT编译后的本地代码,JNI使用的C代码 |
(3)常用参数设置
①-Xms、-Xmx:最小、最大堆内存
②-XX:NewSize、-XX:MaxNewSize:新生代大小、最大新生代大小
③-XX:NewRatio、-XX:SurvivorRatio:新生代与老年代的比例、Eden区与Survivor区的比例
④-XX:MetaspaceSize、-XX:MaxMetaspaceSize:Matespace区大小、最大Matespace区大小(主要是调这个参数)
⑤-XX:+UseCompressedClassPointers:启用压缩类指针
⑥-XX:CompressedClassSpaceSize:压缩类空间的大小,没有设置,默认是1G
⑦-XX:InitialCodeCacheSize:初始化CodeCache的大小
⑧-XX:ReservedCodeCacheSize:CodeCache的最大大小
2、垃圾回收算法
思想:枚举根节点、做可达性分析
根节点:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等
(1)标记-清除——算法分为”标记“和”清除“两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有
缺点:效率不高,标记和清除两个过程的效率都不高;产生碎片,碎片太多会导致提前GC
(2)复制——他将可用内存按容量划分为大小相等的两块,每次只是用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清除掉(Young区就是使用的这种算法)
优缺点:实现简单,运行高效,但是空间利用率低
(3)标记-整理——标记过程仍然与”标记-清除“算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端到边界以外的内存
优缺点:没有了内存碎片,但是整理内存比较耗时
(4)分代收集:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法;而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用”标记-清除“或”标记-整理“算法进行收回。
3、对象分配
(1)对象优先在Eden分配
(2)大对象直接进入老年代:
可以通过设置 -XX:PretenureSizeThresgold 指定多大的对象是大对象
(3)长期存活的对象进入老年代:
长期存活参数设置 -XX:MaxTenuringThreshold;
-XX:PrintTenuringDistribution——发生Young GC时打印一下存活对象年龄的信息
-XX:TargetSurvivorRatio——经过一次Young GC存活的比例
4、垃圾收集器
(1)串行收集器Serial:Serial、Serial Old
参数设置:-XX:+UseSerialGC -XX:+UseSerialOldGC (新生代和老年代中使用)
(2)并行收集器Parallel:Parallel Scavenge、Parallel Old(吞吐量优先)
参数设置:-XX:+UseParallelGC -XX:+UseParallelOldGC
Server模式下的默认收集器
(3)并发收集器Concurrent:CMS、G1(停顿时间优先)
参数设置:-XX:+UseConcMarkSweepGC -XX:UseParNewGC -XX:+UseG1GC
——并行(Parallel):之多余垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适合科学计算、后台处理等弱交换场景。
——并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾收集线程在执行的时候不会停顿用户程序的运行。适合对响应时间有要求的场景,比如:web。
——停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。参数设置:-XX:MaxGCPauseMillis
——吞吐量:花在垃圾收集的时间和花在应用时间的占比。参数设置:-XX:GCTimeRatio=<n>,垃圾收集时间占比:1/(1+n)
垃圾收集器搭配
Q:如何选择垃圾收集器?
A:①优先调整堆的大小让服务器自己来选择
②如果内存小于100M,使用串行收集器
③如果是单核,并且没有停顿时间的要求,串行或者JVM自己选择
④如果允许停顿时间超过1s,选择并行或者JVM自己选择
⑤如果响应时间最重要,并且不能超过1秒,使用并发收集器
· Parallel Collector
-XX:+UseParallelGC手动开启,Server默认开启
-XX:ParallelGCThreads=<N>多少个GC线程
CPU>8 N = 5/8
CPU<8 N = CPU
Parallel Collector Ergonomics(自适应)
-XX:MaxGCPauseMillis=<N>
-XX:GCTimeRatio=<N>
-Xmx <N>
动态内存调整
-XX:YoungGenerationSizeIncrement=<Y> (默认20%)
-XX:TenuredGenerationSizeIncrement=<T>(默认20%)
-XX:AdaptiveSizeDecrementScaleFactor=<D>(默认4%)
· CMS Collector
并发收集、低停顿、低延迟、老年代收集
收集过程:①CMS initial mark:初始标记Root,STW(stop the world);
②CMS concurrent mark:并发标记;
③CMS-concurrent-preclean:并发预清理
④CMS remark:重新标记,STW;
⑤CMS concurrent sweep:并发清除。
缺点:CPU敏感、会产生浮动垃圾、会产生空间碎片
相关参数:-XX:ConcGCThreads 并发的GC线程数
-XX:+UseCMSCompactAtFullCollection Full GC之后做压缩一次
-XX:CMSInitiatingOccupancyFraction 触发FullGC(90%+)
-XX:+UseCMSInitiatingOccupancyOnly 是否动态调整
-XX:+CMSScavengeBeforeRemark FullGC之前先做YGC
-XX:+CMSClassUnloadingEnable 启用回收Perm区(JDK8之前)
iCMS:增量的CMS,适用于单核或者双核
· G1 Collector
JDK7正式使用,新生代和老生代收集器,大内存、停顿时间也短
Region
SATB:Snapshot-At-The-Beginning,它是通过Root Tracing得到的,GC开始时候存活对象的快照。
RSet:记录了其他Region中的对象引用本Region中对象的关系属于points-into结构(谁引用了我的对象)
Young GC
新对象进入Eden区、存活对象拷贝到Survivor区、存活时间达到年龄阈值时,对象晋升到Old区
Mixed GC
不是FullGC,回收所有的Young和部分Old
global concurrent marking(全局并发标记)
①Initial marking phase:标记GC Root,STW;
②Root region scanning phase:标记存活Region;
③Concurrent marking phase:标记存活的对象;
④Remark phase:重新标记,STW;
⑤Cleanup phase:部分STW。
相关参数:
发生时机:InitiatingHeapOccupancyPercent:堆占有率达到这个数值则触发global concurrent marking默认45%;
G1HeapWastePercent:在global concurrent marking结束之后,可以知道区有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC;
G1MixedGCLiveThresholdPercent:Old区的region被回收时的存活对象占比;
G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数;
G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old区的region数量;
常用参数:
-XX:UseG1GC开启G1
-XX:G1HeapRegionSize=n,region的大小,每个大小1-32M,最多2048个
-XX:MaxGCPauseMillis=200 最大停顿时间
-XX:G1NewSizePercent、-XXG1MaxNewSizePercent
-XX:G1ReservePercent=10 保留防止to space溢出
-XX:ParallelGCThreads=n SWT线程数
-XX:ConcGCThreads=n 并发线程数=(1/4)并行
5、可视化GC日志分析工具
(1)在线工具:http://gceasy.io/
(2)GCViewer:
(3)打印日志的相关参数
-XX:+PrintGCDetails、-XX:+PrintGCTimeStamps、-XX:+PrintGCDateStamps、
-Xloggc:$CATALINA_HOME/logs/gc.log、-XX:+PrintHeapAtGC、-XX:+PrintTenuringDistribution
(4)日志格式:Parallel GC、CMS GC、G1 Young GC、G1 Mixed GC(略。。。)
6、GC调优步骤
(1)打印GC日志
(2)根据日志得到关键的性能指标(吞吐量+响应时间)
(3)分析GC原因,调优JVM参数
(4)实战GC调优:
初始设置:-XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log
Parallel GC调优的指导原则:
①除非确定,否则不要设置最大堆内存;
②优先设置吞吐量目标;
③如果吞吐量目标达不到,调大最大内存,不能让OS使用Swap,如果仍然达不到,降低目标;
④吞吐量能达到,GC时间太长,设置停顿时间的目标
G1 GC调优最佳实践:
①年轻代大小:避免使用-Xmn、-XX:NewRatio等显式设置Young区大小,会覆盖暂停时间目标;
②停顿时间目标:暂停时间不要太严苛,其吞吐量目标是90%的应用程序和10%的垃圾回收时间,太严苛会直接影响到吞吐量;
③关于MixGC调优设置:-XX:InitiatingHeapOccupancyPercent、-XX:G1MixedGCLiveThresholdPercent、-XX:G1HeapWastePercent
-XX:G1MixedGCCountTarget、-XX:G1OldCSetRegionThresholdPercent
是否需要切换到G1:①50%以上的堆被存活的对象占用;②对象分配和晋升的速度变化非常大;③垃圾回收时间特别长,超过1秒
7、参考资料
jvm的运行时数据区
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
Metaspace
http://ifeve.com/jvm-troubleshooting-guide-4/
压缩类空间
https://blog.csdn.net/jijijijwwi111/article/details/51564271
CodeCache
https://blog.csdn.net/yandaonan/article/details/50844806
http://engineering.indeedblog.com/blog/2016/09/job-search-web-app-java-8-migration/
GC调优指南:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
如何选择垃圾收集器
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html
G1最佳实践
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
G1 GC的一些关键技术
https://zhuanlan.zhihu.com/p/22591838
CMS日志格式
https://blogs.oracle.com/poonam/understanding-cms-gc-logs
GC之详解CMS收集过程和日志分析
http://www.cnblogs.com/zhangxiaoguang/p/5792468.html
G1日志格式
https://blogs.oracle.com/poonam/understanding-g1-gc-logs
理解G1垃圾收集器日志理解G1垃圾收集器日志
https://blog.csdn.net/zhanggang807/article/details/46011341
GC日志分析工具
http://gceasy.io/
GCViewer
https://github.com/chewiebug/GCViewer
ZGC
http://openjdk.java.net/jeps/333