1.前言
Java作为目前最通用的编程语言之一,而Java底层的JVM是Java编程语言的核心。不管是在企业应用系统,移动终端还是大数据领域都有很大的市场占有率。Java的扁平快受到越来越多的开发青睐,但与C/C++相比,Java语言也有些不足的地方,比如在垃圾回收机制上。什么叫垃圾回收,简单来如,不管是对于Java还是C/C++而言,一切皆对象,当创建对象后,就要分配队或栈占用资源。对象实例化后,不可能一直被引用。这里就会有一个问题,对象核实才不会被引用,不被引用后是否立即进行回收。这个在Java和C/C++中是两种不同的方案的。C/C++需要开发者自己去甄别哪个对象不被引用,然后对这个对象进行回收释放资源。但在Java中是进行自动垃圾回收机制的,虽然Java中通过对象的finalize()方法保留了C/C++这个特性,但Java还是建议开发者将垃圾回收这个工作交给JVM。所以真正的想要提升开发java程序或应用系统的性能,就必须对JVM有深刻理解,这样才会有优化方案。本篇博文以Jdk1.8介绍JVM的核心及常用的一些优化方案。
2.JVM布局
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略 ,保证了 JVM 的高效稳定运行。
2.1.按内存布局
2.2.按线程是否共享布局
2.3.堆区(Heap)
为什么先说堆,以2.1的内存布局为例(红色标记),如果你做过Java开发并对JVM有些了解就会知道堆是内存区域中最大的一块区域,它被所有线程共享,几乎存储着几乎所有的实例对象,几乎所有的实例对象都会在堆上分配(这里为什么要用几乎而不是确定的,因为随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了)。既然是几乎所有对象创建的地方,那么堆自然是垃圾收集器管理的主要区域,因此也被称为GC堆。
a.堆的默认分配
b.可以通过命令查看分配的比例
$:java -XX:+PrintFlagsFinal –version
这个命令会输出几百行,我们这里去看和堆内存分配相关的两个参数
uintx InitialSurvivorRatio=8
uintx NewRatio=2
参数详情:
InitialSurvivorRatio
新生代Eden/Survicor空间的初始比例
NewRatio
Old区/Young区的内存比例
因为新生代是由Eden + S0 + S1
组成的,所以按照上述默认比例,如果eden区内存大小是40M,那么两个survivor区就是5M,整个young区就是50M,然后可以算出Old区内存大小是100M,堆区总大小就是150M。
c.堆区的调整
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。
通过设置如下参数,可以设定堆区的初始值和最大值,比如 -Xms256M-Xmx1024M
,其中 -X这个字母代表它是JVM运行时参数, ms是 memory start的简称,中文意思就是内存初始值, mx 是 memory max的简称,意思就是最大内存。
2.4.元数据区
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
从2.1的流程图及2.2的堆区介绍中可以知道一个Java程序,几乎所有对象都在堆中。对于元数据区,该区是常量池,方法元信息,类元信息的存储及占用资源空间的区域。对于这类信息,一个Java程序启动这些信息所占用的资源量是固定的,相对于堆元数据对程序的性能影响较小。
元数据空间的优化参数:
-XX:MetaspaceSize
分配给Metaspace的初始大小
-XX:MaxMetaspaceSize
分配给Metaspace的最大值,超过就会触发Full GC,辞职默认没有限制,但取决于系统的内存大小,JVM会动态该变该值
-XX:MinMetaspaceSize
在GC之后,最小的Metaspace生育空间容量的百分比,减少该值会导致GC
-XX:MaxMetaspaceFreeRatio
在GC之后,最大的Metaspace生育空间容量的百分比,减少为释放空间会导致 GC
3.对象的生命周期
从上面的介绍中可以知道Java中对象所处的位置,但这还不够,只有真正了解一个对象的生命周期和内存分配流程才能更好的做优化。
绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发 YoungGarbageCollection
,即 YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。Survivor区分为so和s1两块内存空间。每次 YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果 YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了18岁就会成年一样,在JVM中 -XX:MaxTenuringThreshold
参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是15, 可以在Survivor区交换14次之后,晋升至老年代。
4.利用jvisualvm
对JVM GC进行监控
4.1.安装Visual GC
插件
在jdk的bin目录下有一个jvisualvm.exe,这个程序是jvm自带的对jvm的全方位监控,但需要安装对应的插件。这里只介绍对GC的监控。
运行jvisualvm.exe
工具->插件->设置
这里更改插件中心的代理,一般配置为一下代码:
https://visualvm.github.io/archive/uc/8u40/updates.xml.gz
但需要对应你的jdk版本
配置好后再插件中选择可用插件,勾选Visual GC点击安装
4.2.利用jvisualvm对java程序进行JVM GC监控
a.程序
package com.surfilter;
import java.util.LinkedList;
import java.util.List;
public class ChDDLDemo {
public static void main(String args[]) throws Exception{
System.out.println("HelloGC!");
List list = new LinkedList();
for(;;) {
byte[] b = new byte[1024*1024];
Thread.sleep(500);
list.add(b);
}
}
}
该程序的逻辑十分简单,再一个死循环中不断的new新的对象,这个程序最终是会出现异常的。
b.启动程序运用Visual GC监控
当程序启动后可以看到本地有这个程序的类(当然也可以进行远程的java程序的监控),双击这个类,可以看到该程序的一些信息,点击Visual GC,会出现以下信息
监控是实时的,可以看到Metaspace是保持不变的,Old区,Eden区,S0,S1区都是在实时更新的,同时在右侧依次往下可以看到编译的时间,GC的回收的时序关系,S0区的内存时序关系,S1区的内存时序关系,Old区的时序关系以及Metaspace的时序关系。从监控图的实时状态中可以看出与对象的周期架构完全符合,等待该程序出异常,来查看这个监控在出现异常和正常执行之间的区别
现在来看Visual GC的监控图
可以看出Old在上升直到满了导致程序异常,而且整个过程都在频繁的进行GC。如果在生产环境的程序GC出现如上图的情况,肯定性能会打折扣,深圳导致生产环境程序崩溃。如果要避免这些问题,第一要素是使堆在每个区都有空间,第二要素是使程序不会过多的进行GC。这就是JVM优化调优的核心。针对于第二点要从代码规范层面上着手,避免过多的不用对象。第二点则是针对于JVM的参数调优,在实际的开发中,两种情况都会有,但第一种可能更为常见,接下来具体将针对的JVM的参数调优。
5.JVM调优
调优更多的实际应用中进行的,正如某位大神说的,没有业务场景的调优都是耍流氓。这里只是提供一些调优的方案。
如上2.3的c对堆区的大小调整和对元数据的资源占用的调整,还有很多的调优方案
5.1.调优的步骤
a.熟悉业务场景(没有最好的垃圾回收器,只有最合适的垃圾回收器)
b.了解程序的响应时间、停顿时间
c.调研吞吐量 = 用户时间 / 用户时间 + GC时间
d.选择回收器组合
e.计算内存需求
f.设定年代大小、升级年龄
g.设定日志参数
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
观察日志情况
5.2.两个核心的JVM优化方案
a.尽量减少YoungGC
,以减少代码停顿
b.尽量减少FullGC
,以减少系统停顿
在4.2中监控可以看出GC的次数非常多,GC分为YoungGC和FullGC,可见YoungGC和FullGC是非常多的,一天最多 FullGC 一次,最好在系统空闲期(如深夜)
5.3.参数级调优
选项 -Xms300M -Xmx300M -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:PermSize=100M -XX:MaxPermSize=100M
含义为:
- 永久代固定尺寸为 100M;
- 整个堆固定尺寸为 300M,其中“老年代 / 新生代”为-XX:NewRatio=2,所以老年代为 200M,新生代为 100M;
- 新生代总共 100M,其中“Eden / Survivor0”为-XX:SurvivorRatio=8,所以 Eden 为 80M,Survivor0=Survivor1=10M。
可以对以上参数适当的加大
官方资料:http://www.oracle.com/technetwork/articles/java/vmoptions-jsp-140102.html
实际的参数级调优方案很多,可以参照oracle官网