jvm调优如何做?

2021-01-26 17:52发布

12条回答
studentaaa
2楼 · 2021-01-27 10:48

JVM性能调优涉及到方方面面的取舍,往往是牵一发而动全身,需要全盘考虑各方面的影响。但也有一些基础的理论和原则,理解这些理论并遵循这些原则会让你的性能调优任务将会更加轻松。为了更好的理解本篇所介绍的内容。你需要已经了解和遵循以下内容:

1、已了解jvm 垃圾收集器

2、已了解jvm 性能监控常用工具

3、能够读懂gc日志

4、确信不为了调优而调优,jvm调优不能解决一切性能问题

如果对这些不了解不建议读本篇文章。

本篇文章基于jvm性能调优,结合jvm的各项参数对应用程序调优,主要内容有以下几个方面:

1、jvm调优的一般流程

2、jvm调优所要关注的几个性能指标

3、jvm调优需要掌握的一些原则

4、调优策略&示例

 

一、性能调优的层次

为了提升系统性能,我们需要对系统的各个角度和层次来进行优化,以下是需要优化的几个层次。

从上面我们可以看到,除了jvm调优以外,还有其他几个层面需要来处理,所以针对系统的调优不是只有jvm调优一项,而是需要针对系统来整体调优,才能提升系统的性能。本篇只针对jvm调优来讲解,其他几个方面,后续再介绍。

在进行jvm调优之前,我们假设项目的架构调优和代码调优已经进行过或者是针对当前项目是最优的。这两个是jvm调优的基础,并且架构调优是对系统影响最大的 ,我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过jvm调优令其达到一个质的飞跃,这是不可能的。

另外,在调优之前,必须得有明确的性能优化目标, 然后找到其性能瓶颈。之后针对瓶颈的优化,还需要对应用进行压力和基准测试,通过各种监控和统计工具,确认调优后的应用是否已经达到相关目标。

二、jvm调优流程

调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。jvm的调优也不例外,jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量。当然这里的最少是最优的选择,而不是越少越好。

1、性能定义

要查找和评估器性能瓶颈,首先要知道性能定义,对于jvm调优来说,我们需要知道以下三个定义属性,依作为评估基础:

  • 吞吐量:重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。

  • 延迟:其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。

  • 内存占用:垃圾收集器流畅运行所需要 的内存数量。

这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。

2、性能调优原则

在调优过程中,我们应该谨记以下3个原则,以便帮助我们更轻松的完成垃圾收集的调优,从而达到应用程序的性能要求。

1. MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。

2. GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。

3. GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。

我的网名不再改
3楼 · 2021-01-27 13:20

1.数据类型

    java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:它代表的值就是数值本身,而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。

    基本类型包括:byte、short、int、long、char、float、double、boolean、returnAddress

    引用类型包括:类类型、接口类型和数组

2.堆与栈

    堆和栈是程序运行的关键,很有必要它他们的关系说清楚。

  栈是运行时的单位,而堆是存储的单元

   栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储的问题,即数据怎么放,放在哪儿

   在java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关的信息。包括局部变量、程序运行状态、方法返回值等等,而堆只负责存储对象信息。

    为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

     1. 从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。

     2.堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。

     3. 栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力,而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。

     4. 面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实就是编写了数据结构,也编写了处理数据的逻辑。不得不承认,面向对象的设计,确实很美。

    在java中,Main函数就是栈的起始点,也是程序的起始点。

    程序要运行总是有一个起点的。同C语言一样,java中的Main就是那个起点。无论什么java程序,找到main就找到了程序执行的入口。

    堆中存什么?栈中存什么?

    堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4byte的引用(堆栈分离的好处)。

      为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节---需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况---长度固定,因此栈中存储就够了,如果把它存在堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,它们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,java中参数传递时的问题。

     java中的参数传递是传值呢?还是传引用?

    1. 不要试图与C进行类比,java中没有指针的概念。

    2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传递对象本身。

    明确以上两点后。java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。因此,很多书里面都说java是进行传值调用,这点没有问题,而且也简化了C中复杂性。

     但是传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是对象本身,即:修改的是堆中的数据。所以这个修改是可以保持的。

     对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一棵树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。

     堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务的,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得java的垃圾回收称为可能。

     java中,栈的大小通过-Xss来设置,当栈中存储的数据比较多时,需要适当调大这个值,否则会出现 java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。

     java对象的大小

     基本数据类型的大小是固定的,这里就不多说了,对于非基本类型的java对象,其大小就值得商讨。

     在java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看看下面语句:

               Object  ob = new  Object();

     这样在程序中完成了一个java对象的声明,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的java栈中保存引用的所需要空间。而那8byte则是java堆中对象的信息。因为所有的java非基本类型的对象都需要默认继承Object对象,因此不论什么样的java对象,其大小都必须是大于8byte。

     有了Object对象的大小,我们就可以计算其他对象的大小了。

     

       其大小为:空对象大小(8byte)+int大小(4byte)+Boolea大小(1byte)+空Object引用的大小(4byte)=17byte。但是因为java在对对象内存分配时都是以8的整数倍来分的,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。

       这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把它们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是肯恐怖的,它是使用基本类型的N倍(N>2),这些类型的内存占用更是夸张。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,java虚拟机会在存储方面进行相应的优化。

       引用类型

       对象应用类型分为强引用、软引用、弱引用和虚引用

       强引用:就是我们一般声明对象时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收。

       软引用:软引用一般被作为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间,如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。

       弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。

      

       强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存比较受限的情况下作为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。

      

        可以从不同的角度去划分垃圾回收算法:

        按照基本回收策略分

        引用计数(Reference Counting)

        比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

        标记-清楚(Mark-Sweep)

          此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清楚。此算法需要暂停整个应用,同时,会产生内存碎片。

         复制(Copying)

          

       此算法把内存空间划分为两个相等的区域,每次只是用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是比较明显的,就是需要两倍内存空间。

        标记-整理(Mark-Compact)

        此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两个阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆中的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

       按分区对待的方式分

       增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法。

       分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年轻代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

       按系统线程分

       串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。

       并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。

       并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

       如何区分垃圾

       上面说到的“引用计数”法,通过统计控制生成对象和删除对象是的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象,那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的,上面分析的堆和栈的区别,其中栈是真正进行程序执行的地方,所以要获取哪些对象正在被使用,则需要从java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。

        同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其它对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收,而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。

        因此,垃圾回收的起点是一些根对象(java栈、静态变量、寄存器...)。而最简单的java栈就是java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式。

        如何处理碎片

        由于不同java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中 ,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。

        如何解决同时存在的对象创建和对象回收问题

        垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。

        但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,相应应用暂停的时间也会相应的增大。一些相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决。

        为什么要分代

        分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同声明周期的对象可以采取不同的收集方式,以便提高回收效率。

        在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

        是想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是它们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

        如何分代

         如图所示:

         虚拟机中共划分了三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是java类的类信息,与垃圾收集要收集的java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

         年轻代:

             所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

         年老代:

             在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

         持久代:

             用于存放静态文件,如java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize = 进行设置。

        什么情况下触发垃圾回收

        由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC 和 Full GC

        Scavenge GC

           一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

         Full GC

           对整个堆进行整理,包括Young、Tenured 和 Perm。Full GC 因为需要对整个堆进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能导致Full GC:
         . 年老代(Tenured)被写满

         . 持久代(Perm)被写满

         . System.gc()被显式调用

         . 上一次GC之后Heap的各域分配策略动态变化

         分代垃圾回收流程示意

        选择合适的垃圾收集算法

        串行收集器

        用单线程处理所有垃圾回收工作,因为无需多线程交互,所有效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。可以使用 -XX:+UseSerialGC打开。

         并行收集器

        对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用 -XX:+UseParallelGC 打开。并行收集器在 J2SE5.0第六6更新上引入,在java SE6.0中进行了增强 --- 可以对年老代进行并行收集。如果年老代不使用并行收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。使用 -XX:+UseParallelOldGC打开。

        使用 -XX:ParallelGCThreads = 设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。

        此收集器可以进行如下配置:

                最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis = 指定。 为毫秒,如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。

                吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio = 来设定,公式为 1/(1 + N)。例如,-XX:GCTimeRatio = 19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。

        并发收集器

        可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用 -XX:+UseConcMarkSweepGC打开。

        并发收集器主要减少年老代的暂停时间,它在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停。在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。

        并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用 k/N 个可用处理器进行回收,一般情况下 1 <= k <= N / 4。

        在只有一个处理器的主机上使用并发收集器,设置为 incremental mode 模式也可获得较短的停顿时间。

        浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。

        Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。

        启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置 -XX:CMSInitiatingOccupancyFraction = 指定还有多少剩余堆是开始执行并发收集。

        小结

         串行处理器:

         -- 适用情况:数据量比较小(100M左右),单处理器下并且对相应时间无要求的应用。

         -- 缺点:只能用于小型应用。

         并行处理器:

          -- 适用情况:“对吞吐量有高要求”,多CPU,对应用过响应时间无要求的中、大型应用。举例:后台处理、科学计算。

          -- 缺点:垃圾收集过程中应用响应时间可能加长。

         并发处理器:

          -- 适用情况:“对响应时间有高要求”,多CPU,对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。

     

        以下配置主要针对分代垃圾回收算法而言。

        堆大小设置

        年轻代的设置很关键

        JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bit 还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64位操作系统对内存无限制。在Windows Server 2003系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

         典型设置:

                 java -Xmx3550m  -Xms3550m -Xmn2g  -Xss128k

          -Xmx3550m:设置JVM最大可用内存为3550m。

          -Xms3550m:设置JVM初始内存为3550m。此值可以设置与 -Xmx 相同,以避免每次垃圾回收完成后JVM重新分配内存。

          -Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

          -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256k。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

         java  -Xmx3550m  -Xms3550m  -Xss128k  -XX:NewRatio=4  -XX:SurvivorRatio=4  -XX:MaxPermSize=16m  -XX:MaxTenuringThreshold=0

         -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。

         -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。

         -XX:MaxPermSize=16m:设置持久代大小为16m。

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

 

         回收器选择

         JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动的时候加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

        吞吐量优先的并行收集器

        如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学计算和后台处理等。

        典型配置:

            java  -Xmx3800m  -Xms3800m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:ParallelGCThreads=20

          -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集

          -XX:+ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

           

            java  -Xmx3550m  -Xms3550m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

          -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

 

            java  -Xmx3550m  -Xms3550m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

           -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

 

            java  -Xmx3550m  -Xms3550m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100  -XX:+UseAdaptiveSizePolicy

           -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开。 

 

        响应时间优先的并发收集器

        如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

        典型配置:

        java  -Xmx3550m  -Xms3550  -Xmn2g  -Xss128k  -XX:ParallelGCThreads=20  -XX:+UseConcMarkSweepGC  -XX:+UseParNewGC

        -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。

        -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

            

        java  -Xmx3550m  -Xms3550  -Xmn2g  -Xss128k  -XX:+UseConcMarkSweepGC  -XX:CMSFullGCsBeforeCompaction=5  -XX:+UseCMSCompactAtFullCollection 

        -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

        -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片。

 

       常见配置汇总

       堆设置

           -Xms:初始堆大小

           -Xmx:最大堆大小

           -XX:NewSize=n:设置年轻代大小

           -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5。

           -XX:MaxPermSize=n:设置持久代大小

        收集器设置

           -XX:+UseSerialGC:设置串行收集器

           -XX:+UseParallelGC:设置并行收集器

           -XX:+UseParalledlOldGC:设置并行年老代收集器

           -XX:+UseConcMarkSweepGC:设置并发收集器

        垃圾回收统计信息

           -XX:+PrintGC

           -XX:+PrintGCDetails

           -XX:+PrintGCTimeStamps

           -Xloggc:filename

         并行收集器设置

           -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

           -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

           -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+N)

         并发收集器设置

           -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

           -XX:+ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


我是大脸猫
4楼 · 2021-01-27 16:21

直通BAT必考题系列:JVM性能调优的6大步骤,及关键调优参数详

JVM系列

直通BAT必考题系列:7种JVM垃圾收集器特点,优劣势、及使用场景

直通BAT必考题系列:JVM的4种垃圾回收算法、垃圾回收机制与总结

直通BAT必考题系列:深入详解JVM内存模型与JVM参数详细配置

JVM内存调优

 

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。

1.Full GC

会对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比较慢,因此应该尽可能减少Full GC的次数。

2.导致Full GC的原因

1)年老代(Tenured)被写满

调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象 。

2)持久代Pemanet Generation空间不足

增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例

3)System.gc()被显示调用

垃圾回收不要手动触发,尽量依靠JVM自身的机制

在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节,下面详细介绍对应JVM调优的方法和步骤。

JVM性能调优方法和步骤

 

1.监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化。

举一个例子: 系统崩溃前的一些现象:

每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s

FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC

年老代的内存越来越大并且每次FullGC后年老代没有内存被释放

之后系统会无法响应新的请求,逐渐到达OutOfMemoryError的临界值,这个时候就需要分析JVM内存快照dump。

2.生成堆的dump文件

通过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,如果没有启动JMX可以通过Java的jmap命令来生成该文件。

3.分析dump文件

打开这个3G的堆信息文件,显然一般的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:

Visual VM

IBM HeapAnalyzer

JDK 自带的Hprof工具

Mat(Eclipse专门的静态内存分析工具)推荐使用

备注:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。

4.分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。

注:如果满足下面的指标,则一般不需要进行GC:

Minor GC执行时间不到50ms;

Minor GC执行不频繁,约10秒一次;

Full GC执行时间不到1s;

Full GC执行频率不算频繁,不低于10分钟1次;

5.调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

6.不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。

 

cms参数优化步流程

下面我再继续介绍下JVM的关键参数配置(仅用于参考)。

JVM调优参数参考

1.针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值;

2.年轻代和年老代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代。

比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

3.年轻代和年老代设置多大才算合理

1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC

2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率

如何选择应该依赖应用程序对象生命周期的分布情况: 如果应用存在大量的临时对象,应该选择更大的年轻代;如果存在相对较多的持久对象,年老代应该适当增大。但很多应用都没有这样明显的特性。

在抉择时应该根 据以下两点:

(1)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 。

(2)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。

4.在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC 。

5.线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,一般256K就足用。

理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

更多高并发架构设计专题


小小收藏家
5楼 · 2021-01-27 17:13

1、性能定义

要查找和评估器性能瓶颈,首先要知道性能定义,对于jvm调优来说,我们需要知道以下三个定义属性,依作为评估基础:

  • 吞吐量:重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。

  • 延迟:其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。

  • 内存占用:垃圾收集器流畅运行所需要 的内存数量。

这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。

2、性能调优原则

在调优过程中,我们应该谨记以下3个原则,以便帮助我们更轻松的完成垃圾收集的调优,从而达到应用程序的性能要求。

1. MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。

2. GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。

3. GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。


前言

这一期主要介绍进行关于JVM一些概念及经验。后面分章节去讲述相关工具的基本使用。并对针对Tomcat,相关JVM相关主要参数做过一定说明。

优化优先级

整体来讲,系统优化应先优化架构及代码,来解决具体功能点效率问题。最后通过JVM监控工具来发现一些隐藏较为深入的问题。

相关情形

  1. 内存占用并不断增加, 系统压力大情况下Full GC频繁,系统出现卡顿

  2. 线程出现大量等待及死锁, CPU使用率过高, 系统响应慢

  3. 堆(heap)内存不足或类加载导致JVM Crash,系统宕机

出现以上情况,就得使用工具分析JVM来确定问题

JVM内存模型

JDK1.7及以下

JDK1.8下,PermGen替换成Vm MetaSpace

Heap域

  • 全局被所有线程分享

  • 存所有对象及集合对象

方法域

  • 全局被所有线程分享

  • 存所有类的结构定义包含属性,方法及构造函数等

Thread1.N

  • 本地私有栈,一个线程一个栈

  • 保持着所有在Heap域的对象引用(4byte长度)

  • 存储本地局部变量的存储(基础数据类型),程序运行状态,方法返回值

内存泄漏的分类

  • 堆内存泄漏 - 比较常见

  • 持久代内存泄漏

  • 栈内存泄漏

  • 系统资源内存泄漏 -比较常见

线程相关知识

JVM线程状态迁移

线程状态

  • 初始化(New):初期创建,启动后则进入可执行状态

  • 可执行状态(Runnable): 只要获取CPU时间,则开始执行

  • 运行状态(Running): 正在使用CPU执行

  • 阻塞状态(Bloked)

等待阻塞(wait)
同步阻塞(synchronized)
睡眠阻塞(sleep)
Join阻塞: 等待join子线程结束后,主线程才能执行,将异步执行的线程合并为同步的线程
  • 结束状态: 线程执行完毕或者异常退出

性能监控关注点

  1. 系统线程总数

  2. 死锁线程 需要优先解决

  3. 线程Bloked总数数量

线程Bloked多的情况下,考虑对待处理数据进行分片,进行多通道,多线程处理提高系统性能

如果系统处理慢,但CPU占用一直很低,就需要梳理系统处理流程,串行处理该并行处理,并行处理流程提高并发来解决。

线程死锁定义

当两个或者多个线程尝试获取其他资源的锁,而每个线程又陷入无限等待其他资源锁的释放(相互等待),除非一个用户的进程被终止。

几个死锁场景

  • 两个线程相互调用Thread.join(), 导致互相等待同步结束。

慎用线程join操作
  • 当两个线程使用嵌套的同步块时,一个线程占用了另一个线程的必需的锁,互相等待时被阻塞,就有可能出现死锁。 也可能多个线程形成环状锁,比如线程A等待线程B,线程B等待线程C,线程C等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,发现线程C请求的锁被线程A自己持有着。这是它就知道发生了死锁。

  • MySql中两个线程同时对两条记录做先读多写操作

避免死锁

  • 安全状态

能找到一个分配资源的序列能让所有进程都顺利完成
  • 银行家算法

采用预分配策略检查分配完成时系统是否处在安全状态

检测死锁

  • VisualVM(或其他工具)

监控线程状态,如果出现死锁得到相关代码位置
  • 利用死锁定理化间资源分配图来分析死锁的存在

参见以下资料

优化Tomcat的JVM

/bin/catalina.sh

修改JAVA_OPTS参数 这里需要参照机器配置,对JVM进行参数优化

JDK1.7

JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx1024m -XX:NewSize=512m -XX:MaxNewSize=1024M -XX:PermSize=1024m -XX:MaxPermSize=1024m -XX:+DisableExplicitGC"

JDK1.8

JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms1024m -Xmx1024m -XX:NewSize=512m -XX:MaxNewSize=1024M -XX:+DisableExplicitGC"

1.8 版本中已经没有PermSize、MaxPermSize

JAVA8里对metaspace可以在小范围自动扩展永生代避免溢出。

参数说明

  • -Djava.awt.headless

没有设备、键盘或鼠标的模式。
  • -Dfile.encoding

设置字符集
  • -server

vm的server工作模式,对应的有client工作模式。使用“java -version”可以查看当前工作模式
  • -Xms1024m

初始Heap大小,使用的最小内存
  • -Xmx1024m

Java heap最大值,使用的最大内存 经验: 设置Xms大小等于Xmx大小
  • -XX:NewSize=512m

表示新生代初始内存的大小,应该小于 -Xms的值
  • -XX:MaxNewSize=1024M

表示新生代可被分配的内存的最大上限,应该小于 -Xmx的值
  • -XX:PermSize=1024m

设定内存的永久保存区域,内存的永久保存区域,VM 存放Class 和 Meta 信息,JVM在运行期间不会清除该区域

程序加载很多class情况下,超出PermSize情况下

JDK1.7会抛出java.lang.OutOfMemoryError: PermGen space异常

JDK1.8下会抛出 ERROR: java.lang.OutOfMemoryError: Metadata space 异常

  • -XX:MaxPermSize=1024m

设定最大内存的永久保存区域 经验: 设置PermSize大小等于MaxPermSize大小
  • -XX:+DisableExplicitGC

自动将System.gc()调用转换成一个空操作,即应用中调用System.gc()会变成一个空操作,避免程序员在代码里进行System.gc()这种危险操作。System.gc() 除非是到了万不得以的情况下不使用,都交给JVM吧

其他优化参数

  • -XX:SurvivorRatio=2

年轻代中Eden区与Survivor区的大小比值
  • -XX:ReservedCodeCacheSize=256m

保留代码占用的内存容量,无大的影响
  • -Xss1024k

单个线程堆栈大小值,减少这个值可以生成更多线程,但操作系统对于一个进程内的线程数是有限制的,经验值在3000-5000左右
  • -XX:+CMSParallelRemarkEnabled

CMS 垃圾回收算法,对响应时间的重要性需求 大于 对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用
  • -XX:+UseCMSCompactAtFullCollection

在使用concurrent gc 的情况下, 防止 memoryfragmention, 对live object 进行整理, 使 memory 碎片减少。
  • -XX:+UseCMSInitiatingOccupancyOnly

在FULL GC的时候, 对年老代的压缩。CMS是不会移动内存的, 因此这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片。
  • -XX:CMSInitiatingOccupancyFraction=60

使用cms作为垃圾回收, 使用60%后开始CMS收集
  • -XX:+UseGCOverheadLimit

用来限制使用内存,如果不做控制,可能会报出 java.lang.OutOfMemoryError: GC overhead limit exceeded
  • -XX:+UseConcMarkSweepGC

使用CMS内存收集
  • -XX:+UseParNewGC

设置年轻代为并行收集
  • -XX:+HeapDumpOnOutOfMemoryError

  • -XX:HeapDumpPath=/x/dump_tomcat.hprof

JVM会在遇到OutOfMemoryError时拍摄一个“堆转储快照”,并将其保存在一个文件中。
  • -Xloggc:/xx/gc_tomcat.log

gc的日志,如果该日志中出现频繁的Full GC就是有相关的系统问题,如果很少,说明暂时还算正常
  • -XX:+PrintGCDateStamps

输出GC的时间戳(以基准时间的形式)
  • -XX:+PrintGCDetails

输出GC的日志格式
  • -Dnetworkaddress.cache.ttl=60

  • -Dsun.net.inetaddr.ttl=60

设置DNS缓存时间
  • -DautoStartup=false

  • -Dsun.net.client.defaultConnectTimeout=60000

连接建立超时时间
  • -Dsun.net.client.defaultReadTimeout=60000

内容获取超时设置
  • -Djmagick.systemclassloader=no

是否生成缩略图的一个框架的配置
  • -Djava.security.egd=file:/dev/./urandom

最佳实践

export JAVA_OPTS="-server -showversion -Xms2000m -Xmx2000m -Xmn500m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:SurvivorRatio=2 -XX:ReservedCodeCacheSize=256m -Xss1024k -Djava.awt.headless=true -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tomcat_path/logs/dump_tomcat.hprof -Xloggc:/tomcat_path/logs/gc_tomcat.log -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCDetails -Dnetworkaddress.cache.ttl=60 -Dsun.net.inetaddr.ttl=60 -DautoStartup=false -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 -Djmagick.systemclassloader=no -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=UTF-8"

常见JVM异常

  • java.lang.OutOfMemoryError: Java heap space —-JVM Heap(堆)溢出

JVM 在启动的时候会自动设置 JVM Heap 的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)不可超过物理内存。可以利用 JVM提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap 的大小是 Young Generation 和 Tenured Generaion 之和。在 JVM 中如果 98% 的时间是用于 GC,且可用的 Heap size 不足 2% 的时候将抛出此异常信息。

解决方法:手动设置 JVM Heap(堆)的大小。

  • java.lang.OutOfMemoryError: PermGen space —- PermGen space溢出。

jdk1.8 抛出 ERROR: java.lang.OutOfMemoryError: Metadata space 异常

PermGen space 的全称是 Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被 JVM 存放Class 和 Meta 信息的,Class 在被 Load 的时候被放入 PermGen space 区域,它和存放 Instance 的 Heap 区域不同,sun 的 GC 不会在主程序运行期对 PermGen space 进行清理,所以如果你的系统载入很多 CLASS 的话,就很可能出现 PermGen space 溢出。

解决方法: 手动设置 MaxPermSize 大小

  • java.lang.StackOverflowError —- 栈溢出

栈溢出了,JVM 依然是采用栈式的虚拟机。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层”太多了,以致于把栈区溢出了。通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K 的空间(这个大约相当于在一个 C 函数内声明了 256 个 int 类型的变量),那么栈区也不过是需要 1MB 的空间。通常栈的大小是 1-2MB 的。 通常递归也不要递归的层次过多,很容易溢出。

总结

本章主要讲了一些核心知识,主要为了让大家了解系统优化到底优化和解决什么问题,什么是优化的目标。后续章节会讲到tomcat的JMX配置,VisualVM,Tprofile等工具的使用。


前言

这一期主要介绍进行关于JVM一些概念及经验。后面分章节去讲述相关工具的基本使用。并对针对Tomcat,相关JVM相关主要参数做过一定说明。

优化优先级

整体来讲,系统优化应先优化架构及代码,来解决具体功能点效率问题。最后通过JVM监控工具来发现一些隐藏较为深入的问题。

相关情形

  1. 内存占用并不断增加, 系统压力大情况下Full GC频繁,系统出现卡顿

  2. 线程出现大量等待及死锁, CPU使用率过高, 系统响应慢

  3. 堆(heap)内存不足或类加载导致JVM Crash,系统宕机

出现以上情况,就得使用工具分析JVM来确定问题

JVM内存模型

JDK1.7及以下

JDK1.8下,PermGen替换成Vm MetaSpace

Heap域

  • 全局被所有线程分享

  • 存所有对象及集合对象

方法域

  • 全局被所有线程分享

  • 存所有类的结构定义包含属性,方法及构造函数等

Thread1.N

  • 本地私有栈,一个线程一个栈

  • 保持着所有在Heap域的对象引用(4byte长度)

  • 存储本地局部变量的存储(基础数据类型),程序运行状态,方法返回值

内存泄漏的分类

  • 堆内存泄漏 - 比较常见

  • 持久代内存泄漏

  • 栈内存泄漏

  • 系统资源内存泄漏 -比较常见

线程相关知识

JVM线程状态迁移

线程状态

  • 初始化(New):初期创建,启动后则进入可执行状态

  • 可执行状态(Runnable): 只要获取CPU时间,则开始执行

  • 运行状态(Running): 正在使用CPU执行

  • 阻塞状态(Bloked)

等待阻塞(wait)
同步阻塞(synchronized)
睡眠阻塞(sleep)
Join阻塞: 等待join子线程结束后,主线程才能执行,将异步执行的线程合并为同步的线程
  • 结束状态: 线程执行完毕或者异常退出

性能监控关注点

  1. 系统线程总数

  2. 死锁线程 需要优先解决

  3. 线程Bloked总数数量

线程Bloked多的情况下,考虑对待处理数据进行分片,进行多通道,多线程处理提高系统性能

如果系统处理慢,但CPU占用一直很低,就需要梳理系统处理流程,串行处理该并行处理,并行处理流程提高并发来解决。

线程死锁定义

当两个或者多个线程尝试获取其他资源的锁,而每个线程又陷入无限等待其他资源锁的释放(相互等待),除非一个用户的进程被终止。

几个死锁场景

  • 两个线程相互调用Thread.join(), 导致互相等待同步结束。

慎用线程join操作
  • 当两个线程使用嵌套的同步块时,一个线程占用了另一个线程的必需的锁,互相等待时被阻塞,就有可能出现死锁。 也可能多个线程形成环状锁,比如线程A等待线程B,线程B等待线程C,线程C等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,发现线程C请求的锁被线程A自己持有着。这是它就知道发生了死锁。

  • MySql中两个线程同时对两条记录做先读多写操作

避免死锁

  • 安全状态

能找到一个分配资源的序列能让所有进程都顺利完成
  • 银行家算法

采用预分配策略检查分配完成时系统是否处在安全状态

检测死锁

  • VisualVM(或其他工具)

监控线程状态,如果出现死锁得到相关代码位置
  • 利用死锁定理化间资源分配图来分析死锁的存在

参见以下资料

优化Tomcat的JVM

/bin/catalina.sh

修改JAVA_OPTS参数 这里需要参照机器配置,对JVM进行参数优化

JDK1.7

JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx1024m -XX:NewSize=512m -XX:MaxNewSize=1024M -XX:PermSize=1024m -XX:MaxPermSize=1024m -XX:+DisableExplicitGC"

JDK1.8

JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms1024m -Xmx1024m -XX:NewSize=512m -XX:MaxNewSize=1024M -XX:+DisableExplicitGC"

1.8 版本中已经没有PermSize、MaxPermSize

JAVA8里对metaspace可以在小范围自动扩展永生代避免溢出。

参数说明

  • -Djava.awt.headless

没有设备、键盘或鼠标的模式。
  • -Dfile.encoding

设置字符集
  • -server

vm的server工作模式,对应的有client工作模式。使用“java -version”可以查看当前工作模式
  • -Xms1024m

初始Heap大小,使用的最小内存
  • -Xmx1024m

Java heap最大值,使用的最大内存 经验: 设置Xms大小等于Xmx大小
  • -XX:NewSize=512m

表示新生代初始内存的大小,应该小于 -Xms的值
  • -XX:MaxNewSize=1024M

表示新生代可被分配的内存的最大上限,应该小于 -Xmx的值
  • -XX:PermSize=1024m

设定内存的永久保存区域,内存的永久保存区域,VM 存放Class 和 Meta 信息,JVM在运行期间不会清除该区域

程序加载很多class情况下,超出PermSize情况下

JDK1.7会抛出java.lang.OutOfMemoryError: PermGen space异常

JDK1.8下会抛出 ERROR: java.lang.OutOfMemoryError: Metadata space 异常

  • -XX:MaxPermSize=1024m

设定最大内存的永久保存区域 经验: 设置PermSize大小等于MaxPermSize大小
  • -XX:+DisableExplicitGC

自动将System.gc()调用转换成一个空操作,即应用中调用System.gc()会变成一个空操作,避免程序员在代码里进行System.gc()这种危险操作。System.gc() 除非是到了万不得以的情况下不使用,都交给JVM吧

其他优化参数

  • -XX:SurvivorRatio=2

年轻代中Eden区与Survivor区的大小比值
  • -XX:ReservedCodeCacheSize=256m

保留代码占用的内存容量,无大的影响
  • -Xss1024k

单个线程堆栈大小值,减少这个值可以生成更多线程,但操作系统对于一个进程内的线程数是有限制的,经验值在3000-5000左右
  • -XX:+CMSParallelRemarkEnabled

CMS 垃圾回收算法,对响应时间的重要性需求 大于 对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用
  • -XX:+UseCMSCompactAtFullCollection

在使用concurrent gc 的情况下, 防止 memoryfragmention, 对live object 进行整理, 使 memory 碎片减少。
  • -XX:+UseCMSInitiatingOccupancyOnly

在FULL GC的时候, 对年老代的压缩。CMS是不会移动内存的, 因此这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片。
  • -XX:CMSInitiatingOccupancyFraction=60

使用cms作为垃圾回收, 使用60%后开始CMS收集
  • -XX:+UseGCOverheadLimit

用来限制使用内存,如果不做控制,可能会报出 java.lang.OutOfMemoryError: GC overhead limit exceeded
  • -XX:+UseConcMarkSweepGC

使用CMS内存收集
  • -XX:+UseParNewGC

设置年轻代为并行收集
  • -XX:+HeapDumpOnOutOfMemoryError

  • -XX:HeapDumpPath=/x/dump_tomcat.hprof

JVM会在遇到OutOfMemoryError时拍摄一个“堆转储快照”,并将其保存在一个文件中。
  • -Xloggc:/xx/gc_tomcat.log

gc的日志,如果该日志中出现频繁的Full GC就是有相关的系统问题,如果很少,说明暂时还算正常
  • -XX:+PrintGCDateStamps

输出GC的时间戳(以基准时间的形式)
  • -XX:+PrintGCDetails

输出GC的日志格式
  • -Dnetworkaddress.cache.ttl=60

  • -Dsun.net.inetaddr.ttl=60

设置DNS缓存时间
  • -DautoStartup=false

  • -Dsun.net.client.defaultConnectTimeout=60000

连接建立超时时间
  • -Dsun.net.client.defaultReadTimeout=60000

内容获取超时设置
  • -Djmagick.systemclassloader=no

是否生成缩略图的一个框架的配置
  • -Djava.security.egd=file:/dev/./urandom

最佳实践

export JAVA_OPTS="-server -showversion -Xms2000m -Xmx2000m -Xmn500m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:SurvivorRatio=2 -XX:ReservedCodeCacheSize=256m -Xss1024k -Djava.awt.headless=true -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tomcat_path/logs/dump_tomcat.hprof -Xloggc:/tomcat_path/logs/gc_tomcat.log -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCDetails -Dnetworkaddress.cache.ttl=60 -Dsun.net.inetaddr.ttl=60 -DautoStartup=false -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 -Djmagick.systemclassloader=no -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=UTF-8"

常见JVM异常

  • java.lang.OutOfMemoryError: Java heap space —-JVM Heap(堆)溢出

JVM 在启动的时候会自动设置 JVM Heap 的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)不可超过物理内存。可以利用 JVM提供的 -Xmn -Xms -Xmx 等选项可进行设置。Heap 的大小是 Young Generation 和 Tenured Generaion 之和。在 JVM 中如果 98% 的时间是用于 GC,且可用的 Heap size 不足 2% 的时候将抛出此异常信息。

解决方法:手动设置 JVM Heap(堆)的大小。

  • java.lang.OutOfMemoryError: PermGen space —- PermGen space溢出。

jdk1.8 抛出 ERROR: java.lang.OutOfMemoryError: Metadata space 异常

PermGen space 的全称是 Permanent Generation space,是指内存的永久保存区域。为什么会内存溢出,这是由于这块内存主要是被 JVM 存放Class 和 Meta 信息的,Class 在被 Load 的时候被放入 PermGen space 区域,它和存放 Instance 的 Heap 区域不同,sun 的 GC 不会在主程序运行期对 PermGen space 进行清理,所以如果你的系统载入很多 CLASS 的话,就很可能出现 PermGen space 溢出。

解决方法: 手动设置 MaxPermSize 大小

  • java.lang.StackOverflowError —- 栈溢出

栈溢出了,JVM 依然是采用栈式的虚拟机。函数的调用过程都体现在堆栈和退栈上了。调用构造函数的 “层”太多了,以致于把栈区溢出了。通常来讲,一般栈区远远小于堆区的,因为函数调用过程往往不会多于上千层,而即便每个函数调用需要 1K 的空间(这个大约相当于在一个 C 函数内声明了 256 个 int 类型的变量),那么栈区也不过是需要 1MB 的空间。通常栈的大小是 1-2MB 的。 通常递归也不要递归的层次过多,很容易溢出。

总结

本章主要讲了一些核心知识,主要为了让大家了解系统优化到底优化和解决什么问题,什么是优化的目标。后续章节会讲到tomcat的JMX配置,VisualVM,Tprofile等工具的使用。



清屿
8楼 · 2021-01-28 17:19

JVM性能调优有很多设置,这个参考JVM参数即可.

主要调优的目的:

控制GC的行为.GC是一个后台处理,但是它也是会消耗系统性能的,因此经常会根据系统运行的程序的特性来更改GC行为

控制JVM堆栈大小.一般来说,JVM在内存分配上不需要你修改,(举例)但是当你的程序新生代对象在某个时间段产生的比较多的时候,就需要控制新生代的堆大小.同时,还要需要控制总的JVM大小避免内存溢出

控制JVM线程的内存分配.如果是多线程程序,产生线程和线程运行所消耗的内存也是可以控制的,需要通过一定时间的观测后,配置最优结果。


JVM性能调优有很多设置,这个参考JVM参数即可.

主要调优的目的:

控制GC的行为.GC是一个后台处理,但是它也是会消耗系统性能的,因此经常会根据系统运行的程序的特性来更改GC行为

控制JVM堆栈大小.一般来说,JVM在内存分配上不需要你修改,(举例)但是当你的程序新生代对象在某个时间段产生的比较多的时候,就需要控制新生代的堆大小.同时,还要需要控制总的JVM大小避免内存溢出

控制JVM线程的内存分配.如果是多线程程序,产生线程和线程运行所消耗的内存也是可以控制的,需要通过一定时间的观测后,配置最优结果


相关问题推荐

  • 回答 156

    对于每一位才开始接触JAVA的新手来说,先不要管算法和数据结构,大多数简单的程序不需要用到算法和数据结构,所以当你真正需要时再去学习。编程一段时间以后,你就会知道在哪些地方用到他们。这时知道算法的名字并了解它们的功能,然后动手去实践。当我们在去...

  • 回答 93

    2个都很好就业,更关键的是要学得到东西

  • 回答 12
    已采纳

    获取Map集合中所有的key可以通过map集合的keySet()方法获取例如:    Map map = new HashMap();    map.put(xx,xx); //存放数据    //.... 省略    Set set = map.keySet();    //可以通过迭代器进行测试    Iterator iter = set.iter...

  • 回答 56
    已采纳

    不同年龄,不同掌握程度,学历,找工作城市,面试能力这是一个多方面影响的结果,如果是平均值的话,全国平均薪资14k左右

  • 回答 38

    具体学多久,根据自己的学习力,自律性、解决问题能力来决定若系统性学习,跟着讲师的节奏走,大概半年左右,有专业的讲师把课程进行规划,尽心系统学习,有问题,讲师会帮忙解决,学习的效率很高,避免了自学中出现各种问题解决不了,而耽误很多时间,可能会...

  • 回答 23
    已采纳

    (1)idea启动时会有两个快捷方式,安装完后默认生成在桌面的是32位的idea的快捷方式,如果我们使用这个快捷方式运行大项目,一般都会很卡。解决方法是找到idea的安装目录,然后进入bin文件夹,找到名称为idea64的应用程序,右键他生成桌面快捷方式。以后每次...

  • BIO与NIO、AIO的区别2020-05-19 15:59
    回答 4
    已采纳

    IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。一、BIO     在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要...

  • Java方法的命名规则2021-04-06 19:07
    回答 31

    ava是一种区分字母的大小写的语言,所以我们在定义变量名的时候应该注意区分大小写的使用和一些规范,接下来我们简单的来讲讲Java语言中包、类、变量等的命名规范。(一)Package(包)的命名Package的名字应该都是由一个小写单词组成,例如com、xuetang9、compan...

  • 回答 2

    public class Point {    private int x;    private int y;    public int getX() {        return x;    }    public void setX(int x) {        this.x = x;    }    public int getY() {        return y;    } ...

  • 回答 6

    经典版单例模式public class Singleton {        private static Singleton uniqueInstance;//利用一个静态常量来记录singleton类的唯一实例。     private Singleton() {     }     public static  Singleton getInstance()...

  • 回答 3

    哈希表的长度一般是定长的,在存储数据之前我们应该知道我们存储的数据规模是多大,应该尽可能地避免频繁地让哈希表扩容。但是如果设计的太大,那么就会浪费空间,因为我们跟不用不到那么大的空间来存储我们当前的数据规模;如果设计的太小,那么就会很容易发...

  • 回答 14

    1. DOM(Document Object Model)        DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才...

  • 回答 19

    1)作用不同: throw用于程序员自行产生并抛出异常; throws用于声明在该方法内抛出了异常2) 使用的位置不同: throw位于方法体内部,可以作为单独语句使用; throws必须跟在方法参数列表的后面,不能单独使用。3)内容不同: throw抛出一个异常对象,且只能是...

  • 回答 11

    基本执行过程如下:1)程序首先执行可能发生异常的try语句块。2)如果try语句没有出现异常则执行完后跳至finally语句块执行;3)如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。4)catch语句块可以有多个,分别捕获不同类型...

  • 回答 20

    100-199 用于指定客户端应相应的某些动作。 200-299 用于表示请求成功。 300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。 400-499 用于指出客户端的错误。 400 语义有误,当前请求无法被服务器理解。 401 当前请求需要用户验证...

  • 回答 16

    异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译...

没有解决我的问题,去提问