jvm内存分配策略和性能监控

概述

本篇旨在讲清楚jvm的内存分配策略,gc日志阅读,一些常见名词和jdk提供的一些性能监控工具。废话不多说,开始上货。

GC日志阅读

在开发的世界里,阅读日志是最基础的能力,也是解决问题重要的工具。同样阅读gc日志也是解决虚拟机内存的基础技能,通过配置参数-XX:+PrintGCDetails就可以打印gc日志,建议加上参数-Xloggc指定gc日志目录,避免gc日志和console控制台日志混乱造成的阅读困难。
每一种收集器的日志都会略有不同,但会维持一定的共性,以下面一段日志为例:

0.332: [GC (Allocation Failure) [PSYoungGen: 6120K->504K(6144K)] 12535K->12549K(19968K), 0.0066909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.339: [Full GC (Ergonomics) [PSYoungGen: 504K->0K(6144K)] [ParOldGen: 12045K->10615K(13824K)] 12549K->10615K(19968K), [Metaspace: 3473K->3473K(1056768K)], 0.1372999 secs] [Times: user=0.28 sys=0.00, real=0.14 secs] 

最前面的0.332和0.339代表了gc的发生的时间,它的含义是表达虚拟机启动到发生gc的秒数。后面的GC和Full GC代表着垃圾收集的停顿类型,如果是GC代表的是新生代的GC,也称ygc和minor gc,fullgc代表的是对整堆的一个gc。后面括号里的Allocation Failure和Ergonomics代表的是发生gc的原因,分别是eden区域空间不够和parOldGen空间不够导致的gc和fullgc问题。以Full GC为例,接下来的[PSYoungGen、[ParOldGen、[Metaspace代表gc发生的区域,分别是年轻代、老年代、元空间,其名字也是由所使用的gc收集器密切相关,大致如下:

收集器                    显示区域
serial                   DefNew
ParNew                   ParNew
Parallel Scavenge        PSYoungGen
serial old               Tenured
parallel old             ParOldGen
CMS                      CMS

后面方括号内部的 504K->0K(6144K)代表着该区域GC前使用容量-》GC后该区域所使用容量(该区域总容量),方括号之外的12549K->10615K(19968K)则代表gc之前堆中使用容量-》gc后堆中使用容量(堆总容量)。0.1372999 secs这个很简单,代表gc占用时间,单位是秒。

内存分配与回收策略

  • 对象优先在Eden分配
    大多数情况下,对象优先在新生代Eden区中分配。当Eden区域没有足够空间进行分配时,将发生一次Minor GC。虚拟机提供了-XX:+PrintGCDetails用来输出gc日志,此日志会告诉我们垃圾收集行为时的内存日志,并在进程结束后输出当前内存各区域的分配情况。上例子:

    public class TestAllocation {
    
        private static final int _1MB=1024*1024;
    
        public static void main(String[] args) {
            byte[] a1,a2,a3,a4;
            a1=new byte[2*_1MB];
            a2=new byte[2*_1MB];
            a3=new byte[2*_1MB];
            a4=new byte[4*_1MB];
        }
    }

    gc日志如下所示:

"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetails -Xmx20m -Xms20m -Xmn10m 
[GC (Allocation Failure) [PSYoungGen: 6294K->808K(9216K)] 6294K->4912K(19456K), 0.0023349 secs] [Times: user=0.09 sys=0.00, real=0.02 secs] 
Heap
 PSYoungGen      total 9216K, used 7273K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc50670,0x00000000ffe00000)
  from space 1024K, 78% used [0x00000000ffe00000,0x00000000ffeca020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

其中vm配置参数从第一行便可知。新生代分为eden区域和两块survior区域,默认比例为8:1:1,从图中eden:from:to=8192:1024:1024可以得到验证。下来我们就根据gc日志分析下在执行这段程序时,jvm究竟都做了哪些事。
jvm内存分配策略和性能监控_第1张图片
从这张图片可知,在jdk1.8.0_151中,即使跑一个空的main函数,新生代就要占2362k,这个是虚拟机的初始内存占用,好奇宝宝可以通过jmap命令看看到底堆里装了什么。现在让我们回到最开始代码中,会将a1、a2分配在新生代的eden区,此时eden区域为2048k+2048K+2362k=6458k,因为eden区域空间不够,不足以将a3装入,此时触发minor gc,又因为此时a1、a2对象还存活,suivor区域只有1024k,故将a1、a2分配担保到老年代。从日志中可知,经历过一次minor gc新生代还有808k的存活对象,因为a1、a2已经担保到老年代,故这是初始内存中经过gc存活的对象,通过复制算法转移到survivor中。此时eden区域是0k,其中一块survivor是初始内存,老年代存放着a1、a2对象,此时开始继续分配对象内存,因a3+a4

  • 大对象直接进入老年代
    哪怕你从来没有学习过jvm知识,你或许也听说过江湖上流传着大对象直接进入老年代这个传闻。很多人都知道这个知识点,但恐怕大多数人并不能准确的去描述这个分配策略。
    1.何谓大对象?
    所谓大对象就是需要大量连续内存空间的对象,上个例子中的byte数组就是典型的大对象。
    2.参数-XX:PretenureSizeThreshold的作用?
    虚拟机提供了-XX:PretenureSizeThreshold,令大于这个值得对象直接在老年代分配。hotspot可以在年轻代手机内存的收集器有Serial、ParNew、Parallel Scavenge以及G1(G1划分内存区域比较特殊暂不考虑)。其中只有Serial和ParNew收集器可以识别这个参数,Parallel Scavenge是不识别这个参数的,但并不是大对象直接进入老年代分配策略对其就是无效的,在Parallel Scavenge中自有它的实现,大约等于Eden区域一半的对象会被认成大对象。感兴趣的可以来这看看,传送门:链接描述.如果想要使用-XX:PretenureSizeThreshold参数,可以考虑使用ParNew+CMS的组合。给大家展示一个例子:

        public class BigObject {
    
        public static void main(String[] args) {
            byte[] test = new byte[4*1024*1024];
        }
    }

    jvm内存分配策略和性能监控_第2张图片
    从gc日志,我们很容易得出大对象进入老年代这个结论。对了,还需要注意的是XX:PretenureSizeThreshold的单位是k,不能像-Xmx3mb这样直接指定。
    3.为什么大对象要进入老年代?
    在搞清楚这个问题之前,我们首先要去揣摩大师们设计分代算法的意图。设计师们希望新生代的对象多数是朝生夕灭的,故新生代采用复制算法最合适。复制算法的优点是简单,速度快(在存活对象少的情况下),缺点是占内存要发生内存复制。这样做的目的就是避免在eden区和两个survivor区之间发生大量的内存复制。

  • 长期存活的对象将进入老年代
    虚拟机给每个对象定义了一个对象年龄计数器,保存在对象头中的Mark word部分。如果对象在eden出生,经历过一次minor gc仍然活着,并且能被survivor区容纳,将会被移动到survivor区域,并且将gc年龄设置为1,这种对象没经历一次Minor gc ,年龄就增加一岁,当它的年龄增加到一定程度(默认是15岁),就会被晋升到老年代,这个年龄阈值可以通过参数-XX:MaxTenuringThresold来设置.
  • 动态对象年龄判定
    虚拟机并不是永远要求对象的年龄永远达到MaxTenuringThresold才能晋升老年代,如果在survivor空间中相同年龄所有的对象的大小总和大于survivor空间的一半,年龄大于或等于改年龄的对象直接进入老年代,无须等到MaxTenuringThresold要求的年龄。
  • 空间分配担保
    在发生minor gc之间,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,则代表这次gc是安全的。如果不成立,jdk1.7和jdk1.8的版本中会继续检查老年代最大的可用空间是否大于历次晋升到老年代对象的平均大小,如果小于则进行一次full gc,如果大于则尝试进行一次minor gc,如果出现老年代担保失败的情况则会进行一次full gc。

性能监控与故障处理工具

给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。java开发程序员应该都知道jdk的bin目录下有java.exe和javac.exe,但其实bin下面还有很多的小工具都极其实用,是我们排查问题的关键。工具很多,我们挑几个最常用的来说明:

  • jstat

可以用来监视虚拟机各种运行时状态信息的命令行工具。jstat格式命令为:

jstat [ option vmid [interval[s|ms] [count]] ]

option代表我们想要查询的虚拟机信息,vmid就是我们的进程id,interval和count代表查询间隔(默认单位是ms)和次数。jstat工具主要选项如下图所示:
jvm内存分配策略和性能监控_第3张图片
gccause是最常用的一个option之一,假如我们需要每250ms查询一次进程5888的垃圾收集情况,一共查询20次,命令应该为:

jvm内存分配策略和性能监控_第4张图片

S0和S1对应的是新生代的两块suivor区域,E对应的是Eden,O对应的是老年代,M对应的是Klass Metaspace以及Noklass Metaspace两者总共的使用率,CCS对应的是NoKlass Metaspace的使用率,YGC表示的是新生代gc发生的次数,YGCT表示的是新生代gc总共的stop the world的时间,FGC表示的是Full gc的次数,FGCT GCT同YGCT一样就不多说了,LGCC表示上次gc的原因,GCC表示此次gc的原因。需要额外注意的是,当使用CMS作为老年代收集器的时候,每执行一次Old GC,FGC就会增加两次。

  • jinfo

:可以用来实时查看和调整虚拟机的各项参数。
使用jps -v pid可以查看虚拟机启动时显示指定的参数,前提是你要开rmi。如果想要查看未被指定参数的默认值,除了查文档就只能通过jinfo -flag 选项进行查询,当然如果你使用java -XX:+PrintFlagsFinal查看默认值也是一个很好的选择(此命令会打印出所有的默认值),同时你还可以通过jinfo -flag [+|-] name=value或者-flag name=value修改一部分运行期可写的虚拟机参数,如果想要查看那些事运行期可写的参数,可以通过命令java -XX:+PrintFlagsFinal |grep manageable查询。
jvm内存分配策略和性能监控_第5张图片

  • jmap

jmap命令主要用于生成堆转储快照,一般称为dump文件。当然它的作用不仅仅只有这个,还有查询java堆和方法区的详细信息.生成dump文件。其使用方法如下:
jvm内存分配策略和性能监控_第6张图片
生成堆转储快照: jmap -dump:format=b,file=gc.bin pid(线上慎用,如果堆文件较大,会比较耗时,因为要保证dump的准确性,会发生stop the world)
查看堆详细信息,使用回收期、参数配置和分代状况等:jmap -heap pid
查看对象统计信息:jmap -histo pid(如果加上参数:live,jmap -histo:live pid会率先执行一次gc,线上慎用)

  • jhat

jhat命令可与jmap搭配使用,用来分析dump文件,其内置了一个http服务器,生成分析结果,可以在浏览器查看。不过一般都不会用jhat来分析,原因有两点1:不会直接在部署服务器上分析,因为分析工作是一个耗时且消耗硬件资源的过程。2:因为jhat分析确实比较简陋,目前eclispe的mat是分析dump文件最专业的工具,可以到eclispe官网下载插件版,无须安装eclispe。

  • jstack

jstack命令用于生成虚拟机当前时刻的线程快照,一般称为threaddump或javacore文件。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,主要用来定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源长时间等待等都是导致线程长时间停顿的主要原因。
jvm内存分配策略和性能监控_第7张图片

在jdk1.5中, Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有的StackTraceElement对象,使用这个方法可以完成jstack的大部分功能,可以在实际的项目中用这个方法做个管理页面,来随时监控线程堆栈。

  • VisualVM

VisualVmM是jdk发布到目前为止,最强大的运行监视和故障处理数据,他有一个很大的优点不需要被监视的程序基于特殊的agent运行,因此它对应用程序地实际性能影响很小,使得它可以直接应用到生产环境中。它上面有许多好玩的插件,比如Btrace,它可是线上调试的神奇,可以在不停止目标程序运行的前提下,通过hotspot的hotswap技术动态加入原本不存在的调试代码。这个插件等以后有机会了再具体介绍下。

总结

基本上,hotspot系列jvm的理论知识和常用性能监控工具已经介绍完毕,但这些知识只是jvm世界的一点皮毛,如果大家想要学习更多的知识,可以去关注R大,被称为jvm源码化身的男人。传送门:https://www.zhihu.com/people/...。另外阿里的你假笨同学开发了两个特别实用的程序,微信小程序JVMPocket和网站:http://xxfox.perfma.com/,至于它们有什么功效,大家自己去体验吧^_^。

你可能感兴趣的:(jvm内存分配策略和性能监控)