为什么要对jvm进行优化
解决一下问题,例如:
- 在生产环境程序突然卡主,没有任何日志的输出
- 在某一段程序,CPU突然飙升
- 不仅要让程序稳定,更要提升程序的运行速度
jvm的运行参数
1、jvm的参数类型分为三类:分别是:
- 标准参数:指在往后的任何一个jvm的发行版本中都不会随意改变的参数
- -help
- -version
- -X参数(非标准参数):在往后的jvm版本中可能会做出一些改变
- -Xint
- -Xcomp
- -XX参数:使用频率较高的jvm调休参数
- -XX:newSize
- -XX:+UseSerialGC
实验
- 在控制台输入 java -help
- 找到 -D<名称>=<值> 设置系统属性
- 编写java程序
public class TestJVM {
public static void main(String[] args) {
String str = System.getProperty("str");
if(str == null){
System.out.println("是空的");
}else {
System.out.println(str);
}
}
}
- 在根目录下创建文件夹 mkdir /test
- 在该目录下创建文件 TestJVM.java,代码copy
- 编译该文件 javac TestJVM.java
- 查看文件 ll,会发现生成了class文件
- 执行class文件 java -Dstr=你真帅 TestJVM,会得到输出结果为:你真帅
2、标准参数
2.1-server与-client模式
可以通过-server或-client设置jvm的运行参数。
- 它们的区别是Server VM的初始堆空间会大一些,默认使用的是并行垃圾回收器,启动慢运行快。
- Client VM相对来讲会保守一些,初始堆空间会小一些,使用串行的垃圾回收器,它的目标是为了让JVM的启动速度更快,但运行速度会比Serverm模式慢些。
- JVM在启动的时候会根据硬件和操作系统自动选择使用Server还是Client类型的JVM。
- 32位操作系统
- 如果是Windows系统,不论硬件配置如何,都默认使用Client类型的JVM。
- 如果是其他操作系统上,机器配置有2GB以上的内存同时有2个以上CPU的话默认使用server模式,否则
使用client模式。 - 64位操作系统
- 只有server类型,不支持client类型。
3、-X非标准参数
3.1-Xint、-Xcomp、-Xmixed
-Xint:解释编译模式,一边编译一边执行,效率相对较低
java -showversion -Xint TestJVM
-Xcomp:预编译模式,程序先编译完毕,然后执行class文件,效率相对较高。但是这种模式会牺牲编译器性能,原因是-xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就没有意义了。
java -showversion -Xcomp TestJVM
-Xmixed:混合模式,编译器会自动选择模式,也是推荐的模式
4、-XX参数
-XX也是非标准参数,主要用于JVM的调休和debug的操作
-XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型
4.1 boolean类型
- 格式:-XX:[±]<name> 表示启用或者是禁用<name>属性
- 例如:-XX:+DisableExplicitGC表示禁用手动调用gc操作,也就是说调用System.gc()无效
4.2非boolean类型
- 格式:-XX:<name>=<value> 表示<name>属性的值为<value>
- 如:-XX:NewRatio=1表示新生代和老年代的比值
-Xms和-Xmx参数
-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小
-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M
-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始对内存为512M
适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快
例:
java -Xms521m -Xmx2048m TestJVM
5、查看JVM的运行参数
- 运行java命令时打印参数,需要添加-XX:+PrintFlagsFinal参数即可
- java -XX:+PrintFlagsFinal -version
- 从结果中可以看到,参数有boolean类型和数字类型,值的操作符是=或:=,分别代表默认值和被修改的值。
- 运行
java -XX:+PrintFlagsFinal -XX:+ZeroTLAB -version
发现ZeroTLAB的值被修改了
- 查看正在运行的JVM参数
- 如果想要查看正在运行的jvm就需要借助于jinfo命令查看
- 首先,启动一个tomcat用于测试,来观察下运行的jvm参数。
- 通过jps 或者 jps -l 查看java进程
- #查看所有的参数,用法:jinfo -flags <进程id>
例:jinfo -flags 5466 - #查看某一参数的值,用法:jinfo -flag <参数名> <进程id>
例:jinfo -flag MaxHeapSize 5466
得到
-XX:MaxHeapSize=255852544(字节)
- 如果想要查看正在运行的jvm就需要借助于jinfo命令查看
JVM的内存模型
1.不同版本JDK的内存模型
1.1 JDK1.7的内存模型
- Young 年轻区(代)
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。 - Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。 - Perm 永久区
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。 - Virtual区:
最大内存和初始内存的差值,就是Virtual区。
1.2 JDK1.8的内存模型
由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。
2. 通过jstat命令进行查看堆内存使用情况
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [进程id] [间隔时间/毫秒] [查询次数]
2.1 查看class加载统计
[root@node01 ~]# jps
7080 Jps 6219 Bootstrap
[root@node01 ~]# jstat -class 6219
Loaded Bytes Unloaded Bytes Time
3273 7122.3 0 0.0 3.98
说明:
Loaded:加载class的数量
Bytes:所占用空间大小
Unloaded:未加载数量
Bytes:未加载占用空间
Time:时间
2.2 查看编译统计
[root@node01 ~]# jstat -compiler 6219
Compiled Failed Invalid Time FailedType FailedMethod
2376 1 0 8.04 1 org/apache/tomcat/util/IntrospectionUtils setProperty
说明:
Compiled:编译数量。
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法
2.3垃圾回收统计
[root@node01 ~]# jstat -gc 6219
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
9216.0 8704.0 0.0 6127.3 62976.0 3560.4 33792.0 20434.9 23808.0 23196.1 2560.0 2361.6 7 1.078 1 0.244 1.323
#也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次
[root@node01 ~]# jstat -gc 6219 1000 5
说明:
S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old区大小(KB)
OU:Old使用大小(KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB) CCSC:压缩类空间大小(KB) CCSU:压缩类空间使用大小(KB) YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
3.jmap的使用以及内存溢出分析
前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容,如:内存使用情况的汇总、对内存溢出的定位与分析。
3.1 查看内存使用情况
jmap -heap [进程id]
[root@node01 ~]# jmap -heap 6219
- Heap Configuration: #堆内存配置信息
- Heap Usage: # 堆内存的使用情况
- PS Young Generation #年轻代
- PS Old Generation #年老代
3.2 查看内存中对象数量及大小
- 查看所有对象,包括活跃以及非活跃的
jmap -histo <pid> | more
- 查看活跃对象
jmap -histo:live <pid> | more
#对象说明
- B byte
- C char
- D double
- F float
- I int
- J long
- Z boolean
- [ 数组,如[I表示int[]
- [L+类名 其他对象
3.3 将内存使用情况dump到文件中
有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也是支持dump到文件中的。
用法:jmap -dump:format=b,file=dumpFileName <pid>
示例:jmap -dump:format=b,file=/tmp/dump.dat 6219
3.4 使用 jhat命令对dump文件进行分析(用的不多,如果不想看可以跳过)
用法:
命令:jhat -port <端口号> <file>
例:jhat -port 9999 /tmp/dump.dat
在浏览器输入 :ip:9999进行查看
此时会发现有很多的类的全路径,将进度条放到最后
点进去之后,点击查看:
点击OQL,可以使用Example中案例进行查询,不再赘述。
3.5使用mat工具进行查询分析
3.5.1 mat介绍:
MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
官网地址:mat官网地址
3.5.2 安装:略
3.5.3 使用方法:
- 使用sz命令将生成的dump.dat文件下载到本地中
如果没有sz命令执行以下命令进行下载yum install lrzsz
- 打开mat工具,点击 file-> Open Heap Dump 选择刚刚下载好的文件打开
- 查看可能存在内存泄漏的分析
3.5.4 实战:使用mat分析内存泄漏
- 编写应用程序模拟内存溢出
public class TestJVMOutOfMemory {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for(int i=0; i<1000000 ;i++){
String str = "";
for(int j=0;j<1000;j++){
str += UUID.randomUUID().toString();
}
list.add(str);
}
System.out.println("ok");
}
}
在idea中设置jvm的内存(初始堆内存为8M,最大堆内存为8M),并指定输出:
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
运行程序,等待程序报错
Dumping heap to java_pid1704.hprof ...
Heap dump file created [9486082 bytes in 0.018 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOfRange(Arrays.java:4030)
at java.base/java.lang.StringLatin1.newString(StringLatin1.java:715)
at java.base/java.lang.StringBuilder.toString(StringBuilder.java:448)
at TestJVMOutOfMemory.main(TestJVMOutOfMemory.java:11)
Process finished with exit code 1
找到输出的.hprof 文件,使用mat工具打开
3.6 进程的六种状态
- 初始态(NEW)
- 创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
- 运行态(RUNNABLE),在Java中,运行态包括 就绪态 和 运行态。
- 就绪态
- 该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。
- 所有就绪态的线程存放在就绪队列中。
- 运行态
- 获得CPU执行权,正在执行的线程。
- 由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。
- 就绪态
- 阻塞态(BLOCKED)
- 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。
- 而在Java中,阻塞态专指请求锁失败时进入的状态。
- 由一个阻塞队列存放所有阻塞态的线程。
- 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
- 等待态(WAITING)
- 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。
- 也有一个等待队列存放所有等待态的线程。
- 线程处于等待态表示它需要等待其他线程的指示才能继续运行
- 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
- 超时等待态(TIMED_WAITING)
- 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;
- 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;
- 进入该状态后释放CPU执行权 和 占有的资源。
- 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
- 终止态(TERMINATED)
- 线程执行结束后的状态。
3.6.1 jstack命令
有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢?
由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要看下jvm的内部线程的执行情况,然后再进行分析查找出原因。
这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进行快照,并且打印出来:
#用法:jstack
例 :jstack 6219