jvm调优

         Jvm与c++不同,它实现了内存的自动分配和回收,这种机制使程序员无需关注内存的使用和回收,正是这种情况往往造成程序与jvm内存的分配和回收机制不匹配,从而出现各种问题。比如程序代码量很大,虚拟机永久区内存不足,会抛出outofmemoryerror错误,代码中全局大对象很多导致垃圾回收时间变长,应用出现卡顿现象等。当这二者不匹配时,有时候可能是代码编写不合理造成的,例如上面第二个现象。也有时候需要我们调节jvm各个参数,优化内存的分配和回收,使jvm满足我们程序的要求,比如程序代码量很大,我们要调大永久区,才不会抛出outofmemoryerror。总之,无论哪种现象都需要我们充分理解jvm的内存机制的基础上,才能解决问题,上一篇讲了jvm的垃圾回收机制,这一篇我们要充分理解下jvm的内存区域和jvm调优时用到的各个参数。

         根据jvm规范的规定,jvm的内存区域包括:程序计数器、栈、堆、方法区、运行时常量池。这几块区域可以分为三部分:线程堆栈(程序计数器、栈)、堆、非堆(包括方法区、运行时常量池)。

         堆栈

         线程堆栈是表示jvm给每个线程分配的内存大小,jdk5.0以后每个线程分配的内存大小是1         M,这个值的调节是通过-Xss设置。在线程中,每个方法使用和结束就意味着一个栈帧的入栈和出栈,若方法中使用较大深度的递归或者较大循环时,就会导致大量栈帧入栈,内存空间不足,抛出stackoverflowerror错误。因此,若jvm抛出stackoverflowerror错误时,就需要调节-xss参数,扩大栈的空间,或者检查代码,查看是否有较大(或无限)深度递归或循环,如果是代码有问题,修改代码,若代码无问题,需通过-Xss参数扩大栈空间。在jvm默认参数下,栈深度在大多数情况下达到1000到2000完全没有问题,对于正常的方法调用,这个深度是完全够用的。若每个栈的空间变大,在操作系统物理空间内存不变的情况下的,可生成的线程数量就会变少。在32位操作系统下,每个进程可分配的内存大概是2GB,64位操作系统无限制,但是物理内存大小是固定的,是有限制的。因此,当在多线程应用中,每增加一个线程就需要多占用物理内存1M,若物理内存或者进程分配的内存不足时,这是jvm生成新的线程时就会产生outofmemoryerror错误。因为堆或非堆内存不足也会抛出outofmemoryerror错误,因此当出现这种问题,要具体分析是哪部分空间不足,若物理内存(或进程分配的内存)已占满,堆内存和非堆内存空间都在正常的使用范围内,那么问题就出现在堆栈空间不足,要么增加物理内存,要么减少堆空间(这种方式可能不合理)。所以这一部分,如果jvm没有出现错误,保持默认配置即可。

         堆内存

         大家更关注的往往是堆内存的分配和回收,这部分是jvm需要优化的主战场,垃圾回收主要是针对堆内存,jvm垃圾回收的频率和时间是程序运行好坏的重要指标,因为垃圾回收要终止其它正常的工作线程,导致程序停顿。影响Jvm垃圾回收的频率和时间因素,除了垃圾收集器自身的实现机制外,堆内存的大小或各带(堆分代设置)比例设置等也是重要的因素。调节堆内存的参数如下:

-Xms 1024M  //表示堆内存初始化大小,默认是物理内存1/64;

-Xmx 1024M  //表示堆内存最大值,默认是物理内存1/4;

-Xmn 512M  //表示堆中新生代大小;

-XX:NewRadio  //表示新生代与老年代的比例,-XX:NewRadio =2表示比例为1:2;

-XX:SurvivorRadio   //表示新生代中eden区与survivor区的比例,默认比例为8;

        Jvm中堆内存分为新生代和老年代,新生代又分为eden区和两个survivor区,其中eden区和survivor区默认比例是8:1。新生代中垃圾回收采用复制算法,每次对eden区和其中一个survivor区执行垃圾回收,然后将存活的对象复制到另一个survivor区中,因此,新生代只能使用90%的空间。一般来说,对象首先进入新生代的eden区,经过若干次(默认是15次)回收后,若还存活,则进入老年代,这个回收次数相关的参数设置是:

-XX:MaxTenuringThreshold  //表示经过多少次回收,对象进入老年代,默认是15;

对于这个参数,对象并不是都必须经过15次回收后进入老年代,若survivor区中的经过相同回收次数的对象占survivor空间的一半以上时,大于或等于这个次数的对象就可以直接进入老年代。但是,对于一些大对象可以设置直接进入老年代,以避免对新生代中eden区和两个survivor区之间发生大量的内存复制,因为大对象需要足够大的空间,导致新生代空间还相对充足时就提前执行垃圾回收来获取足够的连续空间来安置大对象。Jvm提供了一个参数用来设置大对象直接进入老年代:

-XX:PretenureSizeThreshold  //直接进入老年代对象的值的阈值,-XX:PretenureSizeThreshold=3M,表示大于3M的对象可以直接 进入老年代

        以上是jvm中堆内存设置相关的部分参数,在实际的应用中有些设置可以参考很多的经验总结,比如xms与xmx值设置相同,性能才能最好,因为这样可以避免每次垃圾回收进行内存重新分配,使更多的资源用在具体的应用中。然而也有些设置需要参考具体应用,比如eden区、survivor区和老年代等各空间的大小和比例,是否能满足当前的应用,都需要根据具体应用调整各个区内存的大小和比例。现今,硬件可以说已经不是阻碍程序提高性能的瓶颈了,尤其是内存,目前使用的操作系统基本上都是64位了,16G、32G的内存条已经很普遍了。若堆中某区因空间不足而出现outofmemoryerror错误或者程序反应很慢,我们完全可以增大相应的内存来解决。当然一味的增加内存,使jvm不会抛出outofmemroryerror错误,但是有可能会造成一次需要回收的对象增多,回收时间变长,程序出现明显卡顿,从而影响应用的使用,也是不合理的。因此,我们在对jvm堆内存调优时,在保证内存足够用的同时,还要关注的就是各带之间的比例关系,eden区和survivor区之间的默认比例是8:1,一般按照这个比例设置即可。需要关注的是新生代与老年代之间内存大小比例,新生代不能太小否则会频繁执行minor gc,老年代也不能太小,否则新生代中的对象无法进入老年代,只好频繁执行full gc,总之这个比例,查看了很多资料也没有确定的值,还是要根据实际的应用来确定。

        非堆内存

    code cache

         对于非堆区,jdk7及以前版本包括:code cache和perm gen,jdk8中包括code cache、metaspace和compressed class space。它们有共同的部分就是codecache,首先理解下code cache。在jvm调优时一般不太关注code cache,很少因为它的空间不足造成性能下降,理解code cache,先理解java编译器。对于java编译器,把java文件编译成为class文件,我们称为前端编译,对应的,将class文件中的字节码编译称为机器码称为后端编译,最初,hotspot虚拟机运行时,是对字节码逐条解析,这种方式比较慢,后台hotspot引入了jit编译器,对于那些被频繁使用的方法,即热点代码,jit编译器将这些热点代码编译成本地机器码,保存jvm内存中,虚拟机运行时需要调用这些方法时,直接从内存中获取,这样效率就会更高。上面所讲的被保存的机器码就是code cache。 code cache大小在client模式是32M,在server模式是48M,但是,这个数值在jdk7或以前版本中是这么规定的,在jdk8中好像不是这么规定的,通过jconsole查看code cache大小:

Code cache的大小并不是48M。当code cache占满时,会停止方法的编译,进而影响性能。

    永久区         

        对于perm gen,也就是被称为永久代,在jdk8中已经移除了永久带,但在jdk7中是存在的,若应用还在使用jdk7时,对永久带的调优参数如下:

-XX:PermSize=512M  //永久区初始大小

-XX:MaxPermSize=512M  //永久区最大值

实际应用中,也是将permsize与maxpermsize设置相同值,性能达到最好。知道永久区调节参数后,我们要明白永久区的作用,jvm运行时哪些内容存储在永久区中,这样永久区出现内存溢出时,才能明确地调整。在Hotspot中,一般把方法区称为永久代,方法区中存放两部分内容,一个是类信息(类版本、字段、方法、接口等描述信息),另一个就是常量池,包括字面量(字符串、被final修饰的常量值等)和符号引用。可知,永久区主要存储的就是class文件相关的内容,所以若应用的工程很大,代码量很大,那么永久区被占的空间就很大。永久区内存不足时,jvm会抛出outofmemoryerror:permgenspace错误。需要注意的是在jdk7中已将将字符串常量移动到堆中,不会因字符串常量很大,使永久区不足。永久区中的垃圾回收主要针对两种类型:被废弃的常量和已卸载的类,被废弃的常量就是没有任何地方引用这个字面量了,这个比较简单。已卸载的类相对来说就很复杂了,一个类被称为已卸载的类须满足三个条件:1.该类所有实例已经被回收;2.加载该类的classloader已经被回收;3.该类对应的java.lang.Class对象没有在任何地方被引用。所以,永久区垃圾回收比较困难,所以jvm在对永久区垃圾回收采用的策略是永久区被使用空间达到maxPermSize时,执行full gc,只要永久区在正确的大小值时不执行垃圾回收。总之,永久代、老年代和新生代都称为jvm内存,它们的内存分配和回收受jvm控制,除非当内存不足时,需要向操作系统扩张空间时。

    metaspace区         

        在jdk8中已经不存在永久区了,因此关于永久区调优的参数已经不再适用了,即使用了,jvm会报警但不会出错。代替永久区的是metaspace。原先在永久代中的常量池中的字面量移动到堆中,类信息和符号引用被保留在现在被称为metaspace的区域中,这样做的好处,我想更有利于jvm的内存分配和回收,常量的回收条件毕竟比类的回收更简单。Metaspace内存调节参数:

-XX:MetaspaceSize=100M  //表示metaspace使用空间达到这个值时,执行垃圾回收

-XX:MaxMetaspaceSize=100M  //metaspace的最大值

默认情况下,metaspace是没有最大值的,因为metaspace不是jvm内存的一部分,它是属于本地内存,内存的分配由操作系统负责。一般来说,metaspace不需要设置相关参数,若这个区域内存溢出,说明物理内存已经不足,需要增加内存卡了。

    直接内存         

    Jvm还有一部分内存称为直接内存(direct memory),这部分内存不是jvm规范的内存区域,也不在堆内存中。这一部分内存主要是为nio(new input/output)操作准备的,使用native函数分配堆外内存,然后通过存储在java堆中的directbytebuffer对象作为对这块内存的引用进行操作。直接内存不属于堆内存,这部分若内存不足也会抛出outofmemoryerror:direct buffer memory错误,直接内存通过下面参数调整大小:

-XX:MaxDirectMemorySize  //调整直接内存大小

    垃圾收集器         

        Jvm调优无非就是两点,一是jvm不能抛出异常,比如stackoverflowerror、outofmemoryerror等,二是jvm性能要满足实际的应用,不能出现卡段或长时间不反应等现象,尤其是高并发时。上面讲的内容已经解决了第一点的问题,对于第二点的问题,是关于jvm内存各区之间的内存比例设置和各区垃圾收集器的使用。各区之间的内存比例需要根据实际的情况具体分析,没有一个放之四海皆准的比例参数供我们选择。另一篇文章讲了各个垃圾收集器的原理,本文再次对垃圾收集器进行归纳总结下,hotspot虚拟机使用的垃圾收集器可分为三种类型:

串行收集器,单线程收集器,收集器工作需要停止工作线程;

并行收集器,多线程收集器,收集器工作需要停止工作线程;

并发收集器,多线程收集器,在收集器工作的部分阶段需要停止工作线程;

在hotspot虚拟机中垃圾收集器按照这三种虚拟机类型划分:

串行收集器:serial收集器(用于client模式,基本上被弃用),serial old收集器(用于老年代,采用标记整理算法,也被称为ps marksweep收集器,若老年代采用此收集器,那么新生代只能使用parallel scavenge收集器)。

并行收集器:parnew收集器(用于新生代,采用复制算法,若老年代使用cms收集器那么新生代只能使用这个收集器)、parallelscavenge收集器(用于新生代,采用复制算法,与serialold收集器配合使用)、parallel old收集器(parallel scavenge收集器用于老年代的版本,采用标记-整理算法)。

并发收集器:cms收集器(用于老年代,标记清除算法)。

垃圾收集器调节相关参数:

-XX:+UseSerialGC:在新生代和老年代使用串行收集器

-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParallelOldGC:老年代使用并行回收收集器

-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等,但在CPU数量较多的情况下,设置相对较小的数值也是合理的。

-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间。他的值是一个大于0的整数。收集器在工作时,会调整Java堆大小或者其他参数,尽可能把停顿时间控制在MaxGCPauseMillis以内。

-XX:GCTimeRatio:设置吞吐量大小。它是0-100的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。

-XX:+UseAdaptiveSizePolicy:打开自适应GC策略。在这种模式下,新生代的大小、eden和survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。

-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS并发收集器

-XX:ParallelCMSThreads:设定CMS的线程数量

-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发,默认为68%

-XX:+UseCMSCompactAtFullCollection:设置CMS收集器完成垃圾收集后是否要进行一次内存碎片的整理

-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

-XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收

-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)

-XX:UseCMSInitiatingOccupancyOnlyn:表示只在到达阈值的时候才进行CMS回收

-XX:+CMSIncrementalMode:使用增量模式,比较适合单CPU。增量模式在JDK 8中标记为废弃,并将在JDK 9中彻底移除。

-XX:+UseG1GC:表示使用g1收集器,g1收集器不同于其它收集器,不区分年轻代和老年代。

    实际参数设置

        在实际中对jvm的调优参数设置,以tomcat为例,在bin文件下创建setenv.bat(window操作系统)文件,然后在文件输入以下相应内容格式即可:

setjava_home=D:\Program Files\Java\jdk1.7.0_80

setjava_opts=%java_opts% -server -Xms1024M -Xmx1024M -Xss1M

rem-XX:MetaspaceSize=100M -XX:MaxMetaspaceSize=100M

 

        

 


  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值