一、JAVA运行时数据区
1、堆(-Xmx与-Xms):所有线程共享.
目的:用来存放对象实例。所有对象实例和数组都要在堆上分配内存。JAVA堆是垃圾收集器管理的主要区域。
内存不够时会报OutOfMemoryError:java heap space异常。
堆分为:新生代(Eden from to) 和 老年代
设置新生代大小:-XX:NewSize -XX:MaxNewSize
-Xmn效果等同于-XX:NewSize和-XX:MaxNewSize相等,推荐使用。
设置新生代中from或to区域的比例大小:-XX:SurvivorRatio = Eden/From = Eden/to = 默认8(Eden:From:To = 8:1:1)
设置新生代和老年代的比例:-XX:NewRatio = 老年代/新生代 = 默认2
即: 默认新生代 = 1/3 的堆空间大小
默认老年代 = 2/3 的堆空间大小
调优:由于-Xms的值太小时,JVM为了尽可能保持在其指定的范围内允许,会频繁的进行GC操作。
策略:-Xmx与-Xms设置成一样避免堆自动扩展,以减少GC次数和耗时。
2、方法区(别名:非堆、永久区 , JAVA8及之后的元数据区)( -XX:PermSize -XX:MaxPermSize):所有线程共享。
目的:存储被虚拟机加载的类信息(版本、字段、方法、父类、接口等描述信息)、常量、静态变量、即时编译器编译后的代码等数据。
注意:jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中。
内存不够时会报OutOfMemoryError异常。
运行时常量池也是在方法区中(JDK 7之前),往运行时常量池加内容最简单的方法:String.intern()
方法区溢出可能情况:CGLib动态创建类导致OutOfMemoryError:PermGen space
JDK1.6和JDK1.7 -XX:PermSize -XX:MaxPermSize 默认是64M,一般够用。如果不够可以调整为128M。如果仍然不够,可能是程序问题。
JDK1.8只有元数据区:使用的是堆外直接内存(操作系统的物理内存),默认是系统剩余内存大小。也可以通过-XX:MaxMetaspaceSize指定,
内存不够报错 OutOfMemoryError:Metaspace
3、JAVA虚拟机栈(-Xss):线程隔离。
每个方法执行的时候创建“栈帧”,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
方法调用到结束是入栈和出栈的过程。
局部变量表:编译器完成内存空间分配。存放了编译器可知的各种基本数据类型、对象引用类型和ReturnAddress类型。
如果线程请求的栈深度大于JVM允许的最大深度,那么会报StackOverFlowError异常。
如果扩展栈时没法申请到足够的内存时,那么会报OutOfMemoryError异常。
栈的大小和具体JVM的实现有关,通常在256K~756K之间,默认是1M。默认情况下栈深度在2000-3000是没问题的,对正常调用足够了。
栈空间越大,支持的线程数量就越小。
如果过多的线程导致内存溢出(OutOfMemoryError:ubable to create new native thread),在不能减少线程数的情况下,只能减少最大堆和减少栈容量来换更度多的线程。
---因为操作系统内存 减去 堆内存后,剩余的系统内存无法创建新的线程。
---一般不考虑,因为这个时候一般需要加机器。
4、本地方法栈:线程隔离。
为调用native方法服务的,负责调用底层C程序实现。
也会抛出StackOverFlowError异常和OutOfMemoryError异常。
5、程序计数器(PC及寄存器):线程隔离
每个线程都需要一个独立的程序计数器。负责当前线程所指向的字节码的行号指示器,指示正在执行的JVM字节码指令的地址。
本地方法的执行计数器的值是空。
6、直接内存:不是JVM的一部分,但是会被频繁用到。所有线程共享。-XX:MaxDirectMemorySize控制(默认和堆最大值一样)
NIO会用到:使用Native函数库直接分配堆外内存,然后通过一个存储在JAVA堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
这样避免了Java堆和Native堆中来回复制数据。
直接内存不受JAVA堆大小限制,但是受本机总内存限制。
内存不够时会报OutOfMemoryError异常。
二、对象访问内存方式:使用句柄和直接指针。
1、使用句柄:好处reference中存储的是堆中稳定的句柄地址,对象被移动时只要改变句柄中实例的数据指针。
2、直接指针:reference 指向堆中对象实例数据的地址,速度快,节省了一次指针定位的时间开销。Sun HotSpot使用的是这种方式。
三、垃圾回收GC
1、方法区也可以被GC回收:对常量池的回收和元数据的回收。
虚拟机确认类信息不被使用时,会将其回收。
回收条件至少有两个:所有类的实例被回收且装载该类的ClassLoader被回收
2、垃圾回收算法和思想
(1)引用计数法:每个对象配一个计数器,对一个对象A,只要有一个引用就加1,减少一个引用就减1。计数为0即可回收。
特点:简单,但是无法处理循环引用的情况(根对象 到A和B均不可达,但是AB之间有循环引用,这样AB都不能回收)。
JAVA没有使用此方法。
(2)标记-清除算法:现代垃圾回收算法的基础 - 标记阶段和清除阶段。
标记阶段:通过根节点标记可达对象,未被标记的对象就是垃圾对象。
清除阶段:清除未被标记对象。
缺点:空间碎片(回收后空间不连续,工作效率会低)。
(3)标记-压缩算法(JAVA老年代):对标记-清除算法的优化 - 标记完后将所有存活的对象压缩到内存的一端,之后清理边界外的所有空间。
特点:不产生空间碎片,而又不需要两块相同的内存空间。
JAVA老年代:存活对象多,使用标记-压缩算法。如果采用复制算法,那么复制成本高。
(4)复制算法(JAVA新生代):将原内存分成两块from 和 to,每次只用其中的一块from,垃圾回收时,将from的存活对象复制到to中。
之后,清理from中的所有对象(from已经没有存活对象了,所以可以清理)。 然后,将to作为使用区域。
特点:没有碎片的问题。但是把系统内存折半了。
复制算法适合JAVA的新生代,因为存活对象少、垃圾对象多,复制的效果比较好。
(5)增量算法:前面的算法进行垃圾回收时,所有的线程都会挂住,暂停一切正常的工作,等待垃圾回收的完成。
思想:垃圾回收线程和应用程序线程交替执行,垃圾回收一次只回收一小片内存区域,和应用程序进行线程切换,减少系统停顿时间。
特点:不中断服务,但是存在线程切换和上下文转换的消耗,可能造成吞吐量下降。
(6)新生代:选择复制算法。因为90%对象都很快被回收
新生代分为Eden、From、To区域。
垃圾回收:
Eden第一次满时,Eden区进行垃圾回收:存活的对象放进From区,清空Eden区
Eden再次满时,Eden区和From区同时进行垃圾回收:存活的对象都放进To区,清空Eden区和From区。
Eden再次满时,Eden区和To区同时进行垃圾回收:存活的对象都放进From区,清空Eden区和To区。
每次垃圾回收,From和To区交替进行存放存活对象。
默认存活对象在survivor中每熬过15次Minor GC时,就会被晋升到老年代中。这个15是可以通过-XX:MaxTenuringThreshold 配置的。
大对象,From或To区域放不下,也会直接进入老年代。
(7)老年代:选择标记-压缩算法。因为老年代的对象都是经过多次垃圾回收剩下的对象,可以认为是常驻内存。
(8)注意:堆Dump 和 Dump线程 都会让系统暂停。
四、GC的分类和触发条件
1、新生代GC(Minor GC): Eden空间满了。
2、年代GC(Major GC 或 Full GC):经常会伴随至少一次的Minor GC,但是不绝对。Major GC的速度一般会比Minor GC慢10倍以上。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行。
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存。
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
五.垃圾回收器的选择
1、 Serial串行收集器::独占单线程
-XX:+UseSerialGC 新生代和老年代使用串行回收器。
-XX:SurvivorRatio 设置eden区大小和survivior区大小的比例。
-XX:PretenureSizeThreshold 设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。
-XX:MaxTenureSizeThreshold 设置对象进入老年代的年龄的最大值。每一次Minor
2、 ParNew和ParallelGC都是并行收集器:独占多线程 , 但是ParallelGC关注吞吐量。
-XX:+UseParNewGC 在新生代使用ParNew并行回收器,老年代使用串行回收器。
-XX:ParallelGCThreads设置用于垃圾回收的线程数。
通常情况下可以和CPU数量相等,但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。
默认CPU数量小于9,与CPU数量相等。当大于8核时,等于3 +(5 * cpu数)/8
-XX:+UseParallelGC 新生代使用ParallelGC并行回收器,老年代用串行回收器
-XX:+UseParallelOldGC 新生代使用ParallelGC 老年代使用ParallelOldGC。
-XX:MaxGCPauseMills设置最大垃圾收集停顿时间。
它的值是一个大于0的正数。收集器在工作时,会调整java堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMills以内。
-XX:GCTimeRatio设置吞吐量大小。
它的值时一个0到100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。
-XX:+UseAdaptiveSizePolicy打开自适应GC策略。
在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。
3、 CMS(Concurrent Mark Sweep 并发标记清理) 收集器:非抢占式多线程,适合多核,关注停顿时间
(1) 流程:
初始标记(STW标记根对象)
-> 并发标记(标记所有对象)
-> 预清理(清理准备及控制停顿时间,避免GC和标记重新并行停止时间长)
-> 重新标记(STW:修正并发标记数据)
->并发清理(清理垃圾)
->并发重置(回收后,重新初始化数据,为下次准备)
ps:STW表示stop the world,独占资源,停止程序。即初始标记和重新标记是独占系统资源的,会暂停程序,其他的流程是可以和应用程序一起执行。
说明:初始标记(STW标记根对象) 和 并发标记(标记所有对象)和 重新标记 都是为了标记需要回收的对象。
(2) 默认的并发线程数:( ParallelGCThreads +3 )/4 ParallelGCThreads 表示GC并行时使用的线程数量。 并发表示收集器和应用程序交替进行。
也能通过-XX:ConcGCTheads 或者 -XX:ParallelCMSThreads 手工指定。
(3)CMS是基于标记清楚法的回收器,会产生大量空间碎片。
CMS提供了-XX:CMSFullGCsBeforeCompaction 和-XX:UseCMSCompactAtFullCollection参数设置压缩整理碎片。
-XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS+串行收集器。
-XX:ParallelCMSThreads 设定CMS的线程数量。
-XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后触发CMS回收,默认为68%。
如果内存增长很快,CMS执行过程中出现内存不足,那么CMS回收会失败,虚拟机将启动串行回收器进行回收。
如果内存增长缓慢,可以设置大些,降低CMS的触发频率。如果内存增长快,那么降低这值,避免触发老年代的串行回收器。
-XX:+UseCMSCompactAtFullCollection设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。
-XX:CMSFullGCsBeforeCompaction设定进行多少次CMS垃圾回收后,进行一次内存压缩。
-XX:+CMSClassUnloadingEnabled 如果想让CMS回收器回收持久区(元数据区),可以使用此参数
-XX:CMSInitiatingPermOccupancyFraction当永久区占用率达到这一百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)。
-XX:CMSInitiatingPermOccupancyOnly表示只在到达阈值的时候才进行CMS回收。
-XX:+CMSIncrementalMode使用增量模式,比较适合单CPU。增量模式在JDK8中标记为废弃,并且将在JDK9中彻底移除。
4、 G1(Garbage First Garbage Collector 垃圾优先的垃圾回收器)收集器: 非抢占式多线程,适合多核,关注停顿时间,替代CMS。
思路:将堆划分为一个个区域,每次回收只回收垃圾比例最高的几个区域,以控制一次停顿的时间。
4个阶段:
新生代GC
并发标记周期:思路同CMS,分阶段把可以和应用程序并发的部分提取出来。
--初始标记(非并发):标记从根节点可达的对象,会伴随一次新生代GC.(Eden区被清空,存活对象移入Survivor区)
--根区域扫描(并发): 扫描Survivor区直接可达的老年代区域,并标记这些可达的对象。- 不能和新生代GC同时执行。
--并发标记(并发):并发标记整个堆中存活的对象。
--重新标记(非并发): 对并发标记由于应用程序在运行,所以需要修正。
--独占清理(非并发):计算各个区域存活对象和GC回收比例并排序,识别可供混合回收的区域。
--并发清理(并发):识别和清理完全空闲的区域。
混合回收:
通过并发标记周期后,回收了少量的对象,大部分对象还是未被回收的。但是G1已明确只要哪些区域含有比较多的垃圾对象,在此阶段就可以针对性进行回收。
G1会优先收集垃圾比例较高的区域。这阶段会同时进行年轻代GC和老年代GC.
必要时进行Full GC
混合GC时空间不足,或者新生代GC时survivor区和老年代无法容纳存活对象,那么会导致一次Full GC.
-XX:+UseG1GC 使用G1回收器。
-XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间。默认值200ms
如果设置过短了,那么可能增加GC次数。
-XX:GCPauseIntervalMillis 设置停顿间隔时间。
-XX:InitiatingHeapOccupancyPercent 整个堆使用率达到%多少时,触发并发标记周期的执行。默认45
过小了会频繁GC,过大了会导致Full GC可能性加大。
-XX:ParallelGCThreads 设置并行回收的GC工作线程数量。
5、垃圾回收器组合
(1)串行Serial(新生代) 和 并行Serial Old (老年代)
(2)并行ParNew(新生代) 和 并发CMS(老年代) :响应速度优先,多CPU环境,适合互联网等要求低延迟服务
(3)并行Parallel Scavenge(新生代) 和 并行Parallel Old (老年代):吞吐量优先,适合后台运算和不需要太多交互,是jdk8默认的垃圾收集器
(4)并发G1(新生代和老年代):响应速度优先,替换CMS,是JDK9 默认的垃圾收集器
六、调优参数(具体参数见上面)
1、如果GC执行时间满足以下判断条件,那么GC调优并没那么必须。(也不绝对,根据业务情况)
Minor GC执行迅速(50毫秒以内)
Minor GC执行不频繁(间隔10秒左右一次)
Full GC执行迅速(1秒以内)
Full GC执行不频繁(间隔10分钟左右一次)
2、调试参数打印
-client 指定客户端模式
-server 指定服务器模式(推荐):参数优化,性能更好。
-XX:+PrintGC 简单的日志
-XX:+PrintGCDetails 垃圾回收时打印日志,包含新生代、老年代、持久代的信息。
-XX:+PrintHeapAtGC 相比PrintGCDetails,会打印出前后的堆信息
-XX:+PrintGCTimeStamps打印GC发生的时间,显示的是距离JVM启动后的时间偏移量。
-XX:+PrintGCApplicationStoppedTime 打印由于GC停顿的时间
-XX:+HeapDumpOnOutOfMemoryError OOM时导出堆信息 与 -XX:HeapDumpPath配合使用
-XX:HeapDumpPath=/u02/coreprd/log/gc.hprof 导出堆信息路径
如:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=U02/coreprd/log/gc_$(date "+%Y-%m-%d_%H:%M:%S").hprof
-XX:OnOutOfMemoryError 允许OOM时执行脚本。
-Xmx512m -Xms512m "-XX:OnOutOfMemoryError=/u02/coreprd/printstack.bat %p"
printstack.bat脚本
/u01/install/jdk1.7_40/bin/jstack -F %1 > /u02/coreprd/log/stack_$(date "+%Y-%m-%d_%H:%M:%S").txt
-XX:+PrintVMOptions 虚拟机收到的命令行的显式参数。
-XX:+PrintCommandLineFlags 打印虚拟机收到的命令行的显式和隐式参数。
-XX:+PrintFlagsFinal 打印所有的系统参数(500多行)
-XX:+TraceClassLoading 加载类日志。
-XX:+TraceClassUnLoading 卸载类日志。
-verbose:class 跟踪类的加载和卸载,等同于上面两个参数。
3、实例
JAVA_OPTS="-server -Xmx4g -Xms4g -Xss256k -XX:MaxDirectMemorySize=1G
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=40
-XX:+PrintGCDateStamps
-Xloggc:${LOG_DIR}/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=${LOG_DIR}/java.hprof
-XX:+DisableExplicitGC
-XX:-OmitStackTraceInFastThrow
-XX:+PrintCommandLineFlags
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-Djava.awt.headless=true
-Djava.net.preferIPv4Stack=true
-Djava.util.Arrays.useLegacyMergeSort=true
-Dfile.encoding=UTF-8
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8081
-Dcom.sun.management.jmxremote.rmi.port=8081
-Djava.rmi.server.hostname=10.11.1.100
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false"
七.性能监控工具
1、top命令
2、vmstat命令:cpu、内存、swap、上下文切换、IO情况。
如:vmstat 1 3 每秒采用一次,共3次。
(procs)r:等待运行进程数
(procs)b:处于非中断睡眠状态的进程数
(Memory)swapd:虚拟内存 单位KB
(Memory)free:空闲内存 单位KB
(Memory)buff:用来缓存的内存 单位KB
(Swap)si:磁盘交换到内存的交换页数量 单位KB/秒
(Swap)so:内存交换到磁盘的交换页数量 单位KB/秒
(IO)bi:发送到块设备的块数 单位块/秒
(IO)bo:从块设备接收到的块数 单位块/秒
(System)in:每秒中断数:包括时钟中断
(System)cs:每秒的上下文切换次数
(CPU)us:用户CPU使用时间
(CPU)sy:内核CPU系统使用时间
(CPU)id:空闲时间
3、iostat 监控I/O
iostat 1 3
4、pidstat工具:功能强大的性能检测工具,可以检测线程情况。安装:sudo apt-get install sysstat
-u表示CPU使用率监控(线程和进程都可以)
pidstat -p 1859 -u 1 3 每秒采样一次,合计3次
pidstat -p 1859 1 3 -u -t 参数-t观察线程,比如获取到线程1204
通过jstack获取到线程栈信息找到1204对应的0x4b4
-d表示磁盘IO情况检测
pidstat -p 1859 -d -t 1 3
-r表示内存监控
pidstat -p 1859 -r 1 3
5、jps
jps -m 输出传递给java进程的参数
jps -l 输出主函数的完整路径
jps -v 显示传递给JVM的参数
6、GC信息:jstat -gc 7854
S0C s0(from区)大小kb
S1C s1(from区)大小kb
S0U s0(from区)已用大小kb
S1U s1(from区)已用大小kb
EC eden区大小kb
EU eden区已用大小kb
OC 老年代大小kb
OU 老年代已用大小kb
PC 永久区大小kb
PU 永久区已用大小kb
YGC 新生代GC次数
YGCT 新生代GC耗时
FGC Full GC次数
FGCT Full GC耗时
GCT GC总耗时
7、jmap -dump:format=b,file=/u02/log/heap.hprof 1254 导出文件使用Visual VM 或 MAT工具分析 。 分析还是推荐MAT工具
jmap -histo 1254 >/u02/log/1.txt 对象统计信息。
8、JDK自带的图形工具(在JDK bin目录下):JConsole 、 VisualVM 和 Mission Control
需要加启动参数配置JMX。
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8081
-Dcom.sun.management.jmxremote.rmi.port=8081
-Djava.rmi.server.hostname=10.11.1.100
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
注意:com.sun.management.jmxremote.rmi.port需要指定和com.sun.management.jmxremote.port一样,这样可以少开一个端口的防火墙。
Mission Control:图表非常漂亮,也支持dump,对新生代展示也非常好。对线程展示最好的,可以实时看阻塞的线程和检查死锁 以及线程使用的内存情况。
JConsole:只能看,实时线程的查看非常详细;对新生代划分了eden区、from和To区,可以动态看到其中的变化。
VisualVM(推荐):综合能力最强。能导出线程dump和堆dump以及分析dump文件,是最方便的一个。
推荐:VisualVM + Mission Control 辅助界面查看阻塞线程、检查死锁和分配的内存情况。
9、实战:纯命令行找出JVM中CPU高的线程
(1)找出JVM Pid 2152
(2)找出2152下的cpu高的线程id
ps -mp 2152 -o THREAD,tid,time| sort -n -k1 -r 得到:1587
(3) 线程ID转换为16进制格式: printf “%x ” 1587 得到: 5ad8
(4)根据线程ID查找线程:jstack 2152 | grep 5ad8-A 30