JVM基础

JVM

JVM内存模型

JVM内存模型.png
JVM内存模型.png

各区对比

区域 是否线程共享 是否会内存溢出 功能
被所有线程共享的区域
方法区 存放已被虚拟机加载的类信息、常量、静态变量等数据
虚拟机栈 线程私有,生命周期与线程相同。
创建线程的时候就会创建一个java虚拟机栈。
本地方法栈 虚拟机使用到本地方法服务(native)、线程私有
程序计数器 不会 记录当前线程执行程序的位置。
改变计数器的值来确定执行的下一条指令,比如循环、分支、方法跳转、异常处理,线程恢复都是依赖程序计数器来完成。

堆内存模型

堆内存.png 注意:jdk1.8之后永久代将被移除。

堆内存设置参数

参数 说明
-Xms 初始堆大小
-Xmx 最大堆大小
-XX:NewSize=n 设置年轻代大小
-XX:NewRatio=n 设置年轻代和年老代的比值,如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年老代和的1/4
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n 设置持久代大小

堆内存JVM参数图

堆内存划分.png 注意:jdk1.8之后永久代将被移除。

与GC有关的JVM参数图

jvm-parameter.png
jvm-parameter.png

JVM监测工具

可视化JVM监测工具

名称 说明 基础功能 特殊功能
jvisualvm jdk自带的jvm性能监测工具。 1. 可以通过本地/远程管理、监控jvm虚拟机
2. 可以参考jvm虚拟机参数与系统参数
3. 可以监测CPU、堆、permGen、类、线程等运行信息,并可以对其操作。如:垃圾回收操作,线程信息查看,堆信息查看。
1. 可以进行程序应用快照
2. 可以安装其他监测辅助插件
3. 性能分析,数据抽样
jconsole jdk自带的jvm性能监测工具。 1. 可以通过本地/远程管理、监控jvm虚拟机
2. 可以参考jvm虚拟机参数与系统参数
3. 可以监测CPU、堆、permGen、类、线程等运行信息,并可以对其操作。如:垃圾回收操作,线程信息查看,堆信息查看。
1. 可以查看堆和非堆内存占用情况,查看各代区内存信息
2. 可以查看MBean信息
YourKit Java Profiler 业界领先的Java剖析工具。 1. 可以通过本地/远程管理、监控jvm虚拟机
2. 可以看各函数cpu资源消耗比例
3. 可以监测CPU、内存、类、线程等运行信息
1. GC查看
2. 请求的执行路径查看
JProbe 企业级的Java剖析器。 1. 内存分析、性能分析
2、可以监测CPU、堆、内存、类、线程等运行信息
1. 覆盖分析
2. 自动性能分析和度量报告
JProfiler 一个商业授权的Java剖析工具。 1. cpu分析、内存分析、性能分析
2. 可以监测CPU、堆、内存、类、线程等运行信息。
专用于分析J2SE和J2EE应用程序
jvmstat 图形版的jstat,由Java 官方提供,目前最新版本为3.0。 1. cpu分析、内存分析、性能分析
2. 可以监测CPU、堆、内存、类、线程等运行信息
GC监控

命令行JVM监测工具

标题 说明
jmap 观察运行中的jvm物理内存的占用情况。
基础功能:内存分析、类情况查看。
jps 列出所有的jvm实例。
基础功能:主要用来输出JVM中运行的进程状态信息。
jvmtop 一个轻量级的控制台程序用来监控机器上运行的所有 Java 虚拟机。
jstack 观察jvm中当前所有线程的运行情况和线程当前状态。
基础功能:主要用来查看某个Java进程内的线程堆栈信息。
jstat JVM统计监测工具。
基础功能:观察classloader、compiler、gc相关信息。
hprof 能够展现CPU使用率,统计堆内存使用情况。

jmap

命令格式:

1
2
3
jmap [ option ] pid
jmap [ option ] executable core
jmap [ option ] [server-id@]remote-hostname-or-IP

常用命令:

1
2
3
4
5
6
7
8
# 查看class的实例数目,内存占用,类全名信息
$ jmap -histo 1234

# 查看对内存情况
$ jmap -heap 1234

# 将内存使用的详细情况输出到文件
$ jmap -dump 1234

注:1234为需要被打印配相信息的java进程id,使用该分析工具时,jvm会处于假死状态。

jps

命令格式:

1
jps [options] [hostid]

注:如果不指定hostid就默认为当前主机或服务器。 常用命令:

1
2
3
4
5
6
7
8
9
10
11
# 查看pid
$ jps -q

# 查看传递给main 方法的参数
$ jps -m

# 查看main类或Jar的全限名
$ jps -l

# 查看传入JVM的参数
$ jps -v

jvmtop

常用命令:

1
2
# 查看3456进程
$ ./jvmtop.sh 3456

jstack

命令格式:

1
2
3
jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip

常用命令:

1
2
3
4
5
6
7
8
9
10
11
# 查看pid 
$ jps -q

# 查看java和native c/c++框架的所有栈信息)
$ jstack -m pid

# 查看关于锁的附加信息
$ jstack -l pid

# 当’jstack [-l] pid’没有相应的时候强制打印栈信息
$ jstack -F pid

jstat

命令格式:

1
2
3
4
# vmid:Java虚拟机ID,在Linux/Unix系统上一般就是进程ID
# interval:采样时间间隔。
# count:采样数目。
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]

常用命令:

1
2
# 输出的是GC信息,采样时间间隔为250ms,采样数为4。
$ jstat -gc 21711 250 4

内存溢出

介绍-内存溢出

名词 原因 经过 结果
内存溢出 程序运行过程中无法申请到足够的内存 导致的一种错误 java.lang.StackOverflowError

内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

原因-内存溢出

  1. 内存泄露可能最终会导致内存溢出
  2. 开启大型文件
  3. 从数据库一次拿了太多的数据

类型-内存溢出

内存溢出类型 说明
堆溢出 java.lang.outOfMemoryError:Java heap space
方法区溢出 java.lang.outOfMemoryError:PermGen space
创建线程导致内存溢出 java.lang.OutOfMemoryError:unable to create new native thread
某项操作使用大量内存时发生 java.lang.OutOfMemoryError: GC overhead limit exceeded()
虚拟机栈和本地方法栈溢出 java.lang.StackOverflowError
直接内存溢出 java.lang.OutOfMemoryError

场景-内存溢出

堆内存溢出

说明
堆作用 生成对象实例和数组
溢出原因 生成对象超过最大内存大小
溢出现象 outOfMemoryError:java heap space

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MemoryLeak {
private String[] s = new String[1000];
public static void main(String[] args) throws InterruptedException {
Map<String,Object> m =new HashMap<String,Object>();
int i =0;
int j=10000;
while(true){
for(;i<j;i++){
MemoryLeak memoryLeak = new MemoryLeak();
m.put(String.valueOf(i), memoryLeak);
}
}
}
}

jvm参数:

1
-Xms5m -Xmx5m -Xmn2m -XX:NewSize=1m

方法区内存溢出

方法区作用 | 存放的是类信息、常量、静态变量等 |
溢出原因 | 1. 程序加载的类过多
2. 使用反射、gclib等这种动态代理生成类的技术 |
溢出现象 | outOfMemoryError:permgem space |

jvm参数

1
-XX:PermSize=2m -XX:MaxPermSize=2m

将方法区的大小设置很低即可,在启动加载类库时就会出现内存不足的情况。

线程栈溢出

线程栈作用 | 线程私有 |
溢出原因 | 1. 递归太深
2. 方法调用层级过多 |
溢出现象 | java.lang.StackOverflowError |

代码示例:

1
2
3
4
5
6
7
8
9
10
11
public class StackOverflowTest {
public static void main(String[] args) {
int i =0;
digui(i);
}
private static void digui(int i){
System.out.println(i++);
String[] s = new String[50];
digui(i);
}
}

JVM参数:

1
-Xms5m -Xmx5m -Xmn2m -XX:NewSize=1m -Xss64k

建议-避免内存溢出

  1. 避免内存泄露,内存泄露是内存溢出的诱因。
  2. 类似将文件或数据加载到内存这种场景,需要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

内存泄露

介绍-内存泄露

名词 原因 经过 结果
内存泄露 程序中动态分配内存给一些临时对象,但是对象不会被GC所回收 对象始终占用内存 内存无法释放,占用内存越来越多

原因-内存泄露

根本原因是代码设计存在缺陷导致。例如:

内存泄露原因 说明
长生命周期的对象持有短生命周期对象的引用 这是内存泄露最常见的场景,也是代码设计中经常出现的问题。
例如,在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。
修改hashset中对象的参数值,且参数是计算哈希值的字段 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。
机器的连接数和关闭时间设置 长时间开启非常耗费资源的连接,也会造成内存泄露。

定位方法-内存泄露

内存泄露定位方法 说明
jmap jdk自带的jmap可以获取内存某一时刻的快照,导出为dmp文件后,就可以用Eclipse MAT来分析了,找出是哪个对象使用内存过多。
MemoryAnalyzer
jstat -gc pid 可以显示gc的信息,查看gc的次数,及时间。
其中最后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。
jstat -gccapacity pid 可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是最小perm的内存使用量,PGCMX显示的是perm的内存最大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。
其他的可以根据这个类推,OC是old内纯的占用量。
jstat -gcutil pid 统计gc信息统计。
jstat -gcnew pid 年轻代对象的信息。
jstat -gcnewcapacity pid 年轻代对象的信息及其占用量。
jstat -gcold pid old代对象的信息。
stat -gcoldcapacity pid old代对象的信息及其占用量。
jstat -gcpermcapacity pid perm对象的信息及其占用量。
jstat -class pid 显示加载class的数量,及所占空间等信息。
jstat -compiler pid 显示VM实时编译的数量等信息。
stat -printcompilation pid
jstat -gcutil 15469 1000 300 如果有大量的FGC就要查询是否有内存泄漏的问题了。

建议--避免内存泄露

  1. 尽早释放无用对象的引用
  2. 使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
  3. 尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
  4. 避免在循环中创建对象

死锁

介绍-死锁

名词 原因 经过 结果
死锁(deadlock) 两个进程都在等待对方执行完毕才能继续往下执行 两个进程相互等待 两个进程都陷入了无限的等待中

分析死锁方法

使用jvisualvm分析死锁

jvisualvm.png 【点击】线程Dump jvisualvm-dump.png

使用jstack分析死锁

  1. 通过 jps 命令查看需要查看的Java进程的vmid。例如进程号为7412。
  2. 然后利用 jstack 查看该进程中的堆栈情况,在cmd中输入
1
$ jstack -l 7412

移动到输出的信息的最下面即可得到如下图所示 deadlock.png

使用JConsole分析死锁

  1. 打开jconsole
  2. 连接到需要查看的进程
  3. 打开线程选项卡,然后【点击】左下角的“检测死锁”
  4. jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情

deadlock1.png deadlock2.png

GC

Object被回收的前提条件

没有任何一个指针直接或间接到达该对象。

垃圾分析算法

垃圾分析算法 优点 缺点
引用计数法 简单 两个对象相互引用时,无法被清除
可达性分析算法 主流 -

垃圾收集算法

垃圾收集算法 优点 缺点 实际应用
标记-清除算法 简单 1. 效率低
2. 产生大量碎片
-
复制算法 1. 不会产生碎片问题
2. 大多的jvm的GC都使用
新生代
标记-整理算法 避免了“标记清除算法”和“复制算法”的缺点。 效率低 老年代
分代收集算法 - - 当前商业虚拟机常用

GC收集器

GC收集器 收集器类型 垃圾收集算法 适用场景 说明
Serial收集器 新生代收集器 复制算法 1. 如果应用的堆大小在100MB以内。
2. 如果应用在一个单核单线程的服务器上面,并且对应用暂停的时间无需求。
使用一个线程进行GC,串行,其它工作线程暂停。
ParNew收集器 新生代收集器 复制算法 Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停。
使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
Parallel Scavenge收集器 新生代 复制算法 如果需要应用在高峰期有较好的性能,但是对应用停顿时间无高要求(比如:停顿1s甚至更长)。 吞吐量优先的垃圾回收器,关注CPU吞吐量,即运行用户代码的时间/总时间。
使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾。
Serial Old收集器 老年代收集器 标记整理算法 单线程收集器,串行,使用单线程进行GC,其它工作线程暂停。
Parallel Old收集器 老年代 标记整理算法 如果需要应用在高峰期有较好的性能,但是对应用停顿时间无高要求(比如:停顿1s甚至更长)。 吞吐量优先的垃圾回收器,多线程,并行,多线程机制与Parallel Scavenge差不错,在Parallel Old执行时,仍然需要暂停其它线程。
CMS(Concurrent Mark Sweep)收集器 老年代收集器 标记清除算法 1. 对应用的延迟有很高的要求。
2. 如果内存大于6G请使用G1。
致力于获取最短回收停顿时间(即缩短垃圾回收的时间),多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见Full GC和并发垃圾回收一节),当用户线程内存不足时,采用备用方案Serial Old收集。

设置收集器

参数 说明
-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParalledlOldGC 设置并行年老代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器

JVM调优

何时需要jvm调优

  1. heap 内存(老年代)持续上涨达到设置的最大内存值
  2. Full GC 次数频繁
  3. GC 停顿时间过长(超过1秒)
  4. 应用出现OutOfMemory 等内存异常
  5. 应用中有使用本地缓存且占用大量内存空间
  6. 系统吞吐量与响应性能不高或下降

JVM调优原则

  1. 多数的Java应用不需要在服务器上进行JVM优化
  2. 多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题
  3. 在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合)
  4. 减少创建对象的数量
  5. 减少使用全局变量和大对象
  6. JVM优化是到最后不得已才采用的手段
  7. 在实际使用中,分析GC情况优化代码比优化JVM参数更好

JVM调优目标

  1. GC低停顿
  2. GC低频率
  3. 低内存占用
  4. 高吞吐量

JVM调优量化目标示例:

  1. Heap 内存使用率 <= 70%;
  2. Old generation内存使用率<= 70%;
  3. avgpause <= 1秒;
  4. Full gc 次数0 或 avg pause interval >= 24小时 ;

JVM调优的一般步骤

第1步:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点; 第2步:确定JVM调优量化目标; 第3步:确定JVM调优参数(根据历史JVM参数来调整); 第4步:调优一台服务器,对比观察调优前后的差异; 第5步:不断的分析和调整,直到找到合适的JVM参数配置; 第6步:找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。

JVM分析

Gc日志分析工具

借助GCViewer日志分析工具,可以非常直观地分析出待调优点。

指标 说明
内存占用率 Memory,分析Totalheap、Tenuredheap、Youngheap内存占用率及其他指标
理论上内存占用率越小越好
GC次数、GC时长 Pause ,分析Gc pause、Fullgc pause、Total pause三个大项中各指标
理论上GC次数越少越好,GC时长越小越好

MAT 堆内存分析工具

EclipseMemory Analysis Tools (MAT) 是一个分析Java堆数据的专业工具,用它可以定位内存泄漏的原因。

垃圾回收统计信息

参数 说明
-XX:+PrintGC 打印GC信息
-XX:+PrintGCDetails 打印GC的详细信息
-XX:+PrintGCTimeStamps 打印GC停顿耗时

JVM调优

优化堆内存

空间 命令参数 建议扩大倍数
基准 FullGC后的老年代空间占用
-Xms和-Xmx 堆的初始值和最大值设置相等
3-4倍基准
新生代 -Xmn 1-1.5倍基准
老年代 2-3倍基准
永久代 -XX:PermSize
-XX:MaxPermSize
1.2-1.5倍基准

优化Full GC

年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为MajorGC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。MajorGC的速度一般会比Minor GC慢10倍以上。

触发Full GC的场景 应对策略
System.gc()方法的调用 通过-XX:+DisableExplicitGC来禁止调用System.gc
老年代代空间不足 让对象在Minor GC阶段被回收,让对象在新生代多存活一段时间,不要创建过大的对象及数组
永生区空间不足 增大PermGen空间
GC时出现promotionfailed和concurrent mode failure 增大survivor space
Minor GC后晋升到旧生代的对象大小大于老年代的剩余空间 增大Tenured space 或下调CMSInitiatingOccupancyFraction=60
内存持续增涨达到上限导致Full GC 通过dumpheap 分析是否存在内存泄漏

JVM调优示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 初始化堆内存大小为12GB
-Xms12g

# 堆内存最大值为12GB
-Xmx12g

-XX:PermSize=500m
-XX:MaxPermSize=1000m

# 新生代大小为2400MB,包括 Eden区与2个Survivor区。
-Xmn2400m

# Eden区与一个Survivor区比值为1:1
-XX:SurvivorRatio=1

-Xss512k

# 直接内存。
# 报java.lang.OutOfMemoryError: Direct buffer memory 异常可以上调这个值。
-XX:MaxDirectMemorySize=1G

# 禁止运行期显式地调用 System.gc() 来触发fulll GC。
# 注意: Java RMI的定时GC触发机制可通过配置-Dsun.rmi.dgc.server.gcInterval=86400来控制触发的时间。
-XX:+DisableExplicitGC

-XX:CompileThreshold=8000
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+UseCompressedOops

# 老年代内存回收阈值,默认值为68
-XX:CMSInitiatingOccupancyFraction=60

# CMS垃圾回收器并行线程线,推荐值为CPU核心数
-XX:ConcGCThreads=4

# 设置垃圾最大年龄。
# 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。
# 如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
-XX:MaxTenuringThreshold=10

# 新生代并行收集器的线程数
-XX:ParallelGCThreads=8

-XX:+ParallelRefProcEnabled
-XX:+CMSClassUnloadingEnabled
-XX:+CMSParallelRemarkEnabled

# 当abortable-preclean预清理阶段执行达到这个时间时就会结束
-XX:CMSMaxAbortablePrecleanTime=500

# 指定进行多少次fullGC之后,进行tenured区 内存空间压缩
-XX:CMSFullGCsBeforeCompaction=4

-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSCompactAtFullCollection
-XX:+HeapDumpOnOutOfMemoryError
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/weblogic/gc/gc_$$.log

附录

参数表

说明
S0 Heap上的 Survivor space 0 区已使用空间的百分比
S1 Heap上的 Survivor space 1 区已使用空间的百分比
E Heap上的 Eden space 区已使用空间的百分比
O Heap上的 Old space 区已使用空间的百分比
P Perm space 区已使用空间的百分比
YGC 从应用程序启动到采样时发生 Young GC 的次数
YGCT 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC 从应用程序启动到采样时发生 Full GC 的次数
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
坚持原创技术分享,您的支持将鼓励我继续创作!
0%