精华内容
下载资源
问答
  • JVM的运行原理及优化配置运行原理: 当一个URL被访问时,内存申请过程如下: 1. JVM会试图为相关Java对象在Eden中初始化一块内存区域 2. 当Eden空间足够时,内存申请结束。否则到下一步 3. JVM试图释放在Eden中...

    JVM的运行原理及优化配置

    运行原理:
    当一个URL被访问时,内存申请过程如下:
    1. JVM会试图为相关Java对象在Eden中初始化一块内存区域
    2. 当Eden空间足够时,内存申请结束。否则到下一步
    3. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收);释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区/OLD区
    4. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
    5. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
    6. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”


    jvm的功能模块:

    • 持久区(Perm)
    • 新生代区(YOUNG) 由1个Eden和2个Survivor组成
    • 旧生代区(OLD)
      结构如下图:
      这里写图片描述

    Java堆相关参数:

    参数名 描述
    ms/mx 定义YOUNG+OLD段的总尺寸,ms为JVM启动时YOUNG+OLD的内存大小;mx为最大可占用的YOUNG+OLD内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销
    NewSize/MaxNewSize 定义YOUNG段的尺寸,NewSize为JVM启动时YOUNG的内存大小;MaxNewSize为最大可占用的YOUNG内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销
    PermSize/MaxPermSize 定义Perm段的尺寸,PermSize为JVM启动时Perm的内存大小;MaxPermSize为最大可占用的Perm内存大小。在用户生产环境上一般将这两个值设为相同,以减少运行期间系统在内存申请上所花的开销。
    SurvivorRatio 设置Survivor空间和Eden空间的比例,默认1:6

    内存溢出的可能性:
    OLD段溢出
    1) 设置的内存参数过小(ms/mx, NewSize/MaxNewSize)
    2) 程序问题
    程序无限循环或循环次数过多的创建对象,也有可能单个程序所申请内存过大,在创建对象使用完后没有及时释放对象的空间。
    Perm段溢出
    通常由于Perm段装载了大量的Servlet类而导致溢出
    将PermSize扩大,一般256M能够满足要求 ; 若别无选择,则只能将servlet的路径加到CLASSPATH中,但一般不建议这么处理

    Java中四种垃圾回收算法:

    • –XX:+UseSerialGC
    • –XX:+UseParallelGC
    • –XX:+UseParallelOldGC
    • –XX:+UseConcMarkSweepGC

    第一种为单线程GC,也是默认的GC。,该GC适用于单CPU机器。
    第二种为Throughput GC,是多线程的GC,适用于多CPU,使用大量线程的程序。第二种GC与第一种GC相似,不同在于GC在收集Young区是多线程的,但在Old区和第一种一样,仍然采用单线程。-XX:+UseParallelGC参数启动该GC。
    第三种为Concurrent Low Pause GC,类似于第一种,适用于多CPU,并要求缩短因GC造成程序停滞的时间。这种GC可以在Old区的回收同时,运行应用程序。-XX:+UseConcMarkSweepGC参数启动该GC。
    第四种为Incremental Low Pause GC,适用于要求缩短因GC造成程序停滞的时间。这种GC可以在Young区回收的同时,回收一部分Old区对象。-Xincgc参数启动该GC。


    Heap设定与垃圾回收:

    JVM有2个GC线程。第一个线程负责回收Heap的Young区。第二个线程在Heap不足时,遍历Heap,将Young 区升级为Older区。Older区的大小等于-Xmx减去-Xmn,不能将-Xms的值设的过大,因为第二个线程被迫运行会降低JVM的性能。

    为什么一些程序频繁发生GC?有如下原因:
    A 程序内调用了System.gc()或Runtime.gc()。
    B 一些中间件软件调用自己的GC方法,此时需要设置参数禁止这些GC。
    C Java的Heap太小,一般默认的Heap值都很小。
    D 频繁实例化对象,Release对象。此时尽量保存并重用对象,例如使用StringBuffer()和String()。

    如果你发现每次GC后,Heap的剩余空间会是总空间的50%,这表示你的Heap处于健康状态。许多Server端的Java程序每次GC后最好能有65%的剩余空间。
    经验之谈:
    1.Server端JVM最好将-Xms和-Xmx设为相同值。为了优化GC,最好让-Xmn值约等于-Xmx的1/3[2]。
    2.一个GUI程序最好是每10到20秒间运行一次GC,每次在半秒之内完成[2]。
    注意:
    1.增加Heap的大小虽然会降低GC的频率,但也增加了每次GC的时间。并且GC运行时,所有的用户线程将暂停,也就是GC期间,Java应用程序不做任何工作。
    2.Heap大小并不决定进程的内存使用量。进程的内存使用量要大于-Xmx定义的值,因为Java为其他任务分配内存,例如每个线程的Stack等。
    3.Stack的设定 每个线程都有他自己的Stack。-Xss用来设置 每个线程的Stack大小。 Stack的大小限制着线程的数量。如果Stack过大就好导致内存溢漏。-Xss参数决定Stack大小,例如-Xss1024K。如果Stack太小,也会导致Stack溢漏。

    展开全文
  • JVM运行原理及优化

    万次阅读 多人点赞 2020-07-05 16:50:22
    我们写好代码,是要通过JVM才能运行的 JVM 想要执行一个类,首先要加载类,在加载类之前,需要先编译成字节码class 文件 然后就执行类加载过程,JVM 加载类话,需要类加载器 类加载器是分层级,遵循双亲...

     

     

    • 我们写好的代码,是要通过JVM才能运行的
    • JVM 想要执行一个类,首先要加载类,在加载类之前,需要先编译成字节码class 文件
    • 然后就执行类的加载过程,JVM 加载类的话,需要类加载器
    • 类加载器是分层级的,遵循双亲委派机制,
      • 最上层是Bootstrap ClassLoder,加载java的核心类库,加载java安装目录下的lib目录的class文件
      • 第二层是Ext ClassLoder,加载一些java的其他类库,加载java安装目录下的lib/ext目录下的class
      • 第三层是Application ClassLoder ,应该程序类加载器,这个类加载器是加载我们写的类
      • 如果我们自定义类加载器的话,那就是第四层
      • 类加载器遵循双亲委派机制,就是说,如果要加载一个类,先去为他的父类能不能加载,如果父类上面还有父类,就继续问,直到顶层。然后顶层说加载不了,就下派到子类,如果所有父类都加载不了,那就自己加载。这么做的好处是,不会重复加载一个类
    • 然后说一下类加载的过程,分这么几步,加载,验证,准备,解析,初始化。 
      • 加载的话,就是刚才说的类加载器去加载类
      • 验证阶段,主要是验证加载的字节码是否符合JVM规范,不然随便瞎写JVM也执行不了
      • 准备阶段,主要是给对象申请内存,然后给变量设置初始值,该设置0的设置0,该设置null的设置null
      • 解析阶段,主要是给符号引用变成直接引用,就是把一些变量什么temp,直接换成物理地址,不然执行的时候JVM也不认识temp是啥
      • 初始化阶段:主要是给变量赋值,准备阶段只是设置了初始值,这个是核心阶段,执行类的初始化,如果发现这个类的父类没有初始化,会先暂停,然后去初始化父类,也是走类加载的一套流程,直到父类加载完了,再执行子类的初始化
    • 这是类加载的过程,加载的类是放到了JVM的元数据空间,也就是永久代。

     

     

     

    • 永久代的话,我们JVM参数一般会给设置个256M,这个一定要设置,绝对不能不设置JVM参数,使用默认JVM参数可能就给新生代分配一两百兆,永久代分配的可能也很少,一旦并发量上来,系统扛不住,永久代一般就放点类和常量池,一般给256M够了,如果给小了,可能导致频繁的Full GC,因为永久代如果满了,会触发Full GC,这个是很坑的
    • 类加载到永久代后,会把类交给字节码执行引擎去执行,画图
    • 执行这个操作是线程去执行的,每个线程都配有一个程序计数器Java虚拟机栈

     

    • 因为Java是支持多线程的,所以必须要有程序计数器记录这个线程执行到哪了
    • Java虚拟机栈在执行每个方法的时候,都会创建一个栈帧,main方法也一样
    • 局部变量都放到这个栈帧中,如果这个方法执行完了,局部变量也就失效了
    • 这里的栈帧如果没有执行完时,其实都是GC Root,垃圾回收时,就是根据这里的局部变量的引用和永久代的引用来判断对象是否存活
    • 我们设置JVM参数的时候,一般都会给Java虚拟机栈 1M的大小,一个系统运行最多几百个线程,不用设置太大,浪费内存,也不能设置太小,容易溢出,特别是递归调用的时候
    • 然后局部变量保存的都是对象的地址,地址指向了JVM堆内存

     

    • 如果是使用ParNew + CMS 垃圾回收器的话,堆内存分年轻代老年代是很明确的,不像G1
    • 然后说一下ParNew垃圾回收器
      • 这个垃圾回收器是回收年轻代的,使用的是多线程回收,不像之前的Serial回收器使用单线程回收。
      • 然后ParNew使用的是复制清除算法,把年轻代分为Eden区 和两个Survivor,JVM 参数默认的占比是 8:1:1,系统运行会把对象创建到Eden区,每次YoungGC 会标记存活对象复制到Survivor0中,再次YoungGC时,再把存活对象复制到Survivor1中。系统运行期间会保证一直有一个Survivor是空着的
      • Eden区的占比有时是可以调优的,如果条件有限,没有大内存的机器,然后对象创建的还特别频繁,存活的对象比较多,那就建议把Eden区比例调低一些,让Survivor大一点,宁可Young GC多一些,也不要让Survivor触发了动态年龄审核或者放不下存活对象。如果放不下那就把这批对象扔到老年代了,Full GC是很慢的。如果是调低Eden,YoungGC会很频繁,但是YoungGC特别快,我通过jstat 看,回收100M垃圾大概也就1ms,所以,如果内存实在不够,降低Eden去比例也不是不可以。但是如果有条件的话最好的话还是加大新生代内存,毕竟YoungGC也是要Stop the World的。
    • 然后继续说ParNew ,它非常适合回收年轻代内存。因为年轻代一般存活的对象是很少的,大多数都是刚创建出1毫秒就变成了垃圾,所以把极少数存活的对象标记出来,复制成本还是很低的,如果像老年代那样采用标记清除算法,那就太慢了

     

    • 然后说一下老年代的垃圾回收器CMS,这个垃圾回收器是使用的 标记-清除 + 整理 算法,我们一般在JVM参数会指定这算法和整理的频率,JVM参数默认是,标记-清除,5次之后,才会去整理内存空间,让对象整齐排列。但是这个默认参数不太好,这样做会有大量的内存碎片,如果某一次从年轻代晋升一个大对象,老年代居然找不到一块连续的内存,就会触发Full GC,那就坑了。我们会把那个值调成0,就是每次CMS垃圾回收后,都会整理内存,虽然每次的回收时间会多一些,但是不会出现内存碎片。

     

    • CMS 垃圾回收分为4个步骤
      • 第一步是初始标记:初始标记的话,只标记GC root直接引用的对象,只有很少一部分,这个阶段需要STW,但是影响不大,这个过程特别快。这个过程也可以优化,JVM 有个参数是初始标记阶段多线程标记,减少STW时间,正常是单线程标记的。
      • 第二步是并发标记:这个阶段是不需要STW的,是和系统并行的处理,系统继续运行,然后垃圾回收线程去追踪第一步标记的GC root,这一步是很耗时的,但是不影响程序执行。因为在垃圾回收时是允许系统继续创建对象的,所以这个过程会有新的对象进来,也会有标记存活的但是现在变成垃圾,这些有改动的对象JVM都会记下来,等待下一步处理。这一步有一个缺点,并发清理时也有这个问题,就是会占用CPU资源。如果是一个4核的机器,那会占用一个CPU去垃圾回收,公式是(cpu核数 + 3)/4。所以一般CPU资源负载特别高的时候,就俩情况,要不是程序的线程太多了。要不就是频繁FullGC,导致的。
      • 第三步是重新标记:重新标记阶段,会把并发标记阶段有改动的对象重新标记,这一步需要STW,不过也是比较快的,因为改动的对象不会特别多,但是要比第一步慢因为要重新判断找个对象是否GC可达。这里也可以通过JVM参数优化,可以通过参数控制,让CMS在重新标记阶段之前尽量触发一次Young GC(尽量YoungGC是因为可能新生代可能刚刚YoungGC不久,那此时就没必要再一次YoungGC了)这样做的好处是,改动的对象中从存活变为垃圾的那部分,就被清理掉了,缩短STW时间。虽然YoungGC也会造成停顿,但是YoungGC一般频率是比较快的,早晚都要执行,现在执行一举两得。
      • 第四步是并发清理,并发清理是和系统并行的,不需要STW。这个阶段是清理前几个阶段标记好的垃圾。
      • 最后,我们通过JVM参数设置,每次Old GC后都重新整理内存,整理阶段会把老年代零零散散的对象排列到一起,减少内存碎片。
    • 在说ParNew + CMS调优之前,我们先说下JVM的几种GC,Young GC,Old GC,Full GC
      • Young GC 和 Old GC 我上面都已经说过了
      • 再说下 Full GC 吧,Full GC就是全面回收整个堆内存,包括新生代、老年代、永久带。整个过程极其的慢,一定要减少Full GC的次数,一般出现频繁的Full GC有几种情况,我们要避免出现这几种情况
        • 第一种是,内存分配不合理,导致Survivor放不下,或者触发了动态年龄审核机制,频繁的往老年代放对象。
        • 第二种,有内存泄漏问题,导致老年代大部分空间被占用,回收都回收不掉,导致每次新生代晋升一点点对象,就放不下了,触发Full GC
        • 第三种,大对象,一般是代码层面的问题,创建了太多的大对象,大对象是直接放入老年代的, 大对象过多会导致频繁触发Full GC
        • 第四种,永久代满了,触发Full GC,我们JVM参数设置256M基本够了,如果不出现代码层面的bug ,一般不会出现这种情况
        • 第五种,有人在代码里误调用了System.gc(),写了这个方法后,如果有机会,JVM就会发生一次Full GC。不过JVM参数可以禁止这种情况,不允许主动调用,我们要加上
    • 一般什么情况下我们要警觉是不是频繁的Full GC了
      • 第一种情况,CPU负载折线上升,特别高
      • 第二种情况,系统卡死了,或者系统处理请求极慢
      • 第三种情况,如果公司有监控系统,会报警。。
    • 然后再说一下ParNew + CMS 调优的问题吧。如果一个系统需要JVM调优,那其实说白了就是Stop the World 太久了,导致系统太卡了。我们说的调优,其实就是减少STW的时间,让系统没有明显的卡顿现象。
    • 然后分析下,需要STW的有几个地方。YoungGC,和Old GC的两个阶段。但是YoungGC一般STW时间特别短,Old GC时间一般会是Young GC的几倍到几十倍,而且占用CPU资源严重。所以,我们优化的重点是让系统减少Old GC的次数。最好让系统只有YoungGC,没有Old GC,更没有Full GC
    • 所以,优化的重点就是尽量不要让对象进入老年代。如果对象进不去老年代,想Full GC都难。这是JVM调优的重点,对象进入老年代的情况也有几种
      • 第一种,对象经过15次YoungGC,依然是存活的,那晋升老年代
        • 这个其实我们是可以优化一下的,因为如果系统1分钟或者30秒一次YoungGC,那没必要非得让对象存活十几分钟才进入老年代,一般存活个两三分钟,这个对象大概率就是要存活很久的了。所以,我们当时是调低了这个参数的,设置了5。不然这个对象一直存活,然后在两个Survivor里来回复制,如果这个对象小一点还好,如果这个对象挺大的,那容易触发Survivor的动态年龄审核机制,让一大批对象进入老年代。所以,该进入老年代的对象,就让他赶紧进去。
      • 第二种,Young GC后存活的对象大小超过Survivor 的50%,那就会触发动态年龄审核机制,如:1岁、2岁、3岁、4岁的对象加起来大于Survivor 的50%,那大于等于4岁的对象全部进入老年代。
      • 第三种,Young GC后存活的对象大于Survivor的大小,那这一批对象直接全部进入老年代,特别坑。
      • 第四种,大对象直接进入老年代,这个JVM参数里是可以设置的,一般我们都设置1M,大于1M的对象进入老年代,一般很少有1M的对象,一般都是个大数组,或者map。
    • 第一种情况和第四种情况,一般是可控的。所以想要优化的话,主要是要在Survivor的大小这块下功夫。我们要避免动态年龄审核和Survivor放不下的情况。要想保证这点,我们就要知道,我们系统的高峰时期,JVM中每秒有多少对象新增,每次YoungGC存活了多少对象。这就需要用 jstat 了。
    • 首先要使用 jstat -gc PID 1000 1000
      • 找到JVM的PID,然后每秒打印一次JVM的内存情况,如果系统访问量比较小,每秒的增长不是很明显,那就把每次的间隔时间调大一点,比如一分钟打印一次
      • 通过这行命令,我们可以看到当时的内存使用情况,有几个列比较重要的数据
      • S0C:Survivor0 的大小
      • S1C:Survivor1 的大小
      • S0U:Survivor0 使用了多少
      • S1U:Survivor1 使用了多少
      • EC:Eden 区的大小
      • EU:Eden 区使用了多少
      • OC:老年代的大小
      • OU:老年代使用了多少
      • MC:永久代的大小
      • MU:永久代使用了多少
      • YGC:YoungGC次数
      • YGCT:YoungGC的总耗时
      • FGC:Full GC次数
      • FGCT:Full GC的总耗时
    • 一般使用 jstat 优化,重点观察这几个指标
      • Eden 区对象的增长速度
        • 上面的几列,通过一行数据是看不出来Eden 区每秒增长多少数据的,所以我们才每秒打印一次,通过上一秒和下一秒EU的数据就可以推断出每秒增长了多少。这个数据进来多打印几行,取个平均值。
      • Young GC 频率
        • 我们我们知道系统启动时间,用YGC的大小除也能算,但是谁没事记得系统什么时候启动的。而且如果我想看高峰时期某一段时间的呢,就看不了了。看几十天的平均值也没什么意义。所以这个高峰时段YoungGC的频率是通过,Eden的大小,除以Eden区对象的增长速度来算的,Eden区对象增长速度,我们已经知道了。
      • Young GC 耗时
        • 这个YoungGC耗时,我们取平均值就行,用YGCT除以YCG,时间除以次数就是每次的耗时。如果说就像看高峰时段的,因为CPU等使用率比较高,可能会影响回收时间,也可以单独看几次的YoungGC,算出时间。
      • Young GC 后多少对象存活
        • 这个指标还是比较重要的,我们要确定每次存活的对象Survovir到底能不能放得下。我们要保证每次存活的对象要小于Survivor的50%,否则就会触发动态年龄审核机制。
      • 老年代对象增量速度
        • 老年代对象增长速度,决定了Old GC的频率。发生Old GC后,FGC那一列也会增长,FGC那一列其实是FullGC 和Old GC的总和。经过优化后的JVM,每次YoungGC不应该进入太多的对象,不进入或者每次进入几兆是比较好的。这个指标我们也要分多次观察,因为只看一次YoungGC晋升的大小是片面的。我们现在已经知道了YoungGC的频率,如果是3分钟一次,那我们就3分钟打印一次内存情况。jstat -gc PID 180000 100,取多次晋升大小的平均值就行。如果晋升的对象特别多,我们需要分析这些对象为什么会进入老年代,上面我说了有四种情况会晋升老年代,到底是哪种情况。是Survivor不够大,还是大对象太多了,或者有内存泄漏导致对象回收不掉,进入了老年代。这个还要具体分析一下的。如果是Survivor太小,我们很轻易就能看出来,如果每次Young GC后S区都是0,那说明存活的对象太多,S区放不下,都进入了老年代。如果S区不是0,有一部分,但是每次回收进入老年代都很多,就有可能是触发动态年龄审核,这个最好再通过GC日志看一下,通过JVM参数可以让系统打印每次GC的日志。如果出现内存泄漏,数据一般是这样的,发现每次FGC次数加1后,老年代并没有多少数据被回收掉,占用了很多。这就大概率是内存泄漏,导致老年代回收不掉。如果是大对象,数据会这样显示,发现及时没有Young GC,OU也会一直在涨,因为大对象是不用经过年轻代的直接进入老年代。如果内存泄漏和大对象的情况,我们可以用 jmap 打印一份内存快照,用MAT工具分析一下到底是什么对象特别大,通过分析出来的堆栈信息就可以定位到代码的位置。
      • Full GC 频率多高
        • 看这个频率和看YoungGC的频率是一样的,可以看高峰时期某几次的平均值。这个Full GC是很耗时的,Full GC的频率我们最好控制在一天1次或者几天一次的范围。特别是对时效性要求比较高的系统,一定要减少Full GC次数。
      • 一次Full GC 的耗时
        • 这个可以取平均值,也可以取某一段的。我们会发现这个Full GC的耗时是YoungGC的好多倍
    • ParNew + CMS 的原理和优化大概就是这么样的,下面我说一下现在比较流行的G1回收器
    • 有人说G1比ParNew +CMS好,可以全面取代,没必要用ParNew和CMS了,我觉得不是这样的。G1有G1的优点,但是也有缺点。我们选择垃圾回收器还是要根据系统的实际情况来看。但是ParNew+CMS的确有不足的地方,如果某些系统使用的机器是大内存,16G、32G,那每次GC都要等Eden区放满了才执行垃圾回收,一次回收好几G的垃圾,那太慢了,可能停顿时间几十上百毫秒,Full GC甚至要几秒。那就太坑了,不可以接收。这个时候就必须用G1了。
    • 那什么情况下用ParNew+CMS呢,它又什么优点
      • 它的优点就是我们可以优化到极致,极致到没有Full GC只有YoungGC。但是G1不行,我们对G1的优化只能是尽可能的优化预定的停顿时间,其他的我们没法参与太多,因为它什么时候YoungGC我们都不确定。
      • G1的内存使用率是没有ParNew +CMS高的,G1有这么一个机制,如果G1的某一个Region存活对象达到了85%,那就不会去回收这个Region,但是那15%呢。如果是垃圾也回收不掉了。
      • G1的掌控性没有ParNew + CMS好,说白了就是心里没底。我们使用ParNew + CMS可以很确定多久YoungGC,对象增长速度等等等等吧,我们都能看到。但是G1什么时候垃圾回收我们都不知道,如果出现了内存泄漏,如果不是几个G的内存泄漏,我们也很难察觉出来。使用ParNew + CMS可以放心一些,不用搞个活动心惊胆战的。
    • 所以总结来说,如果是4核8G的机器,尽量还是用ParNew + CMS垃圾回收器,如果是大内存机器,就是用G1。
    • 然后说一下G1的原理吧,G1把堆内存平均分成了多个大小相同的Region,我们首先要设置堆内存的大小,然后G1会根据堆大小除以2048,分成2048个大小相同的Region。G1也是有年轻代、老年代的概念,但是只是概念。没有ParNew+CMS分的那么清楚。G1里的年轻代和老年代都是基于Region的,某些Region属于年轻代,某些Region属于老年代,由G1动态控制。但是现在属于年轻代的Region并不永远都是年轻代,如果年轻代的Region被回收了,下次这个Region可能就存放老年代的数据了。所以,G1的年轻代和老年代都是动态的,但是也有个上限。系统刚开始运行时,会给年轻代分配5%的Region来存放对象,年轻代最多可以占用60%的Region,这60%可以通过JVM参数指定,默认是60%,不过这个一般默认就好,如果达到了目标值,就会强制触发YoungGC
    • G1的年轻代也是分Eden和Survivor的,因为G1整体使用的都是复制回收算法。只是某些Region属于Eden,某些Region属于Survivor,系统新创建的对象会被分配到属于Eden的Region,如果垃圾回收就把存活对象复制到Survivor中。
    • G1的一个特点就是我们可以设置一个预期的停顿时间,也就是STW的时间,比如,某个系统的时效性要就特别高,每次GC我只允许STW的5ms,那我们就可以通过JVM参数设置成5ms的停顿,这样G1在垃圾回收时,就会把时间控制在5ms以内
    • G1的垃圾回收不一定是年轻代满了,或者老年代满了才去回收。如果是那样,就和ParNew+CMS没区别了,大内存机器也要STW好久。G1是基于每个Region的性价比去回收的,比如,Region1里有20M对象,回收2ms,Region2里有50兆对象回收要4ms。如果我们设置系统停顿时间为5ms,那G1会在要求的时间内,尽可能回收更多的对象,它会选择Region2,因为性价比更高。所以,我们系统运行,一直往Eden放对象,如果G1觉得,此时回收一下垃圾,差不多要5ms,那可能G1就回去回收,不会等到年轻代占用60%才去回收。
    • G1中年轻代的对象什么情况下会进入老年代
      • 其实和ParNew + CMS整体上是差不多的,只有大对象的处理不一样
      • 1、YoungGC存活的对象Survivor放不下
      • 2、YoungGC存活的对象达到Survivor的50%,触发动态年龄审核
      • 3、对象到达了15岁,进入老年代
      • G1中大对象不会进入老年代,而是专门有一部分Region存放大对象用。如果一个Region放不下大对象,那就会横跨几个Region来存放。
    • G1的Old G也不是我们能控制的,G1会根据自己的判断觉得该回收的时候就会回收,不过也是基于复制算法的
    • G1的混合回收,如果老年代占比45%,就会触发混合回收,回收整个堆内存,但是混合回收也是会控制在我们设置的停顿时间的范围内的,如果时间不够,就会分多次回收。混合回收有点和CMS的回收类似
      • 第一步,初始标记
        • 初始标记需要STW,这一步只标记GC Root直接应用的对象,速度很快
      • 第二步,并发标记
        • 和系统并行,深入的追踪GC Root,标记所有存活的对象,此时系统新创建的对象会被JVM记录,这一步不需要STW
      • 第三步,重新标记,重新标记第二步有改动的对象,要STW。因为只有一小部分改动,速度很快
      • 第四步,混合回收,只有这一步和CMS不一样,CMS这里的回收时和系统并行的。但是G1的混合回收需要STW。混合回收不仅会回收老年代,还会回收新生代和大对象。如果一次性全回收掉,那时间就太久了,可能达不到我们设置的预期停顿时间,所以G1这里是分几批来回收的,回收一次,系统运行一会,然后再回收一次。JVM参数可以设置这个值,分几次去回收,默认值是8次,分8次回收。混合回收还有一个参数我们可以设置,就是空闲的Region达到百分之多少,停止回收,默认是5%
    • G1何时会触发Full GC,其实G1的混合回收就相当于ParNew + CMS的Full GC了,因为回收了所有的区域,只不过回收时间可以控制在我们指定的范围内。但是G1的Full GC就没法控制了,可能要卡顿特别久才能回收完。什么情况下会出现呢,因为G1的整体是基于复制算法的,如果回收的过程中,发现存活对象找不到可以复制的Region,放不下了。那就Full GC,开始单线程标记、清理、整理空闲出一批Region,这个过程很慢
    • 然后说一下G1的优化,G1比较智能,我们可以参与优化的点很少,我们只能合理的设置停顿时间,不要太小也不要太大,太小GC会太频繁,每秒都在GC。太大的话,停顿时间太久了也不好。
    • 平时我们选择垃圾回收器要根据不同的场景具体去分析,该使用那个。没有绝对的好坏。优化也没有一个统一的标准。比如YoungGC和Full GC多久一次好,YoungGC、Full GC耗时多久比较好。这个还是看系统的,只要不影响系统使用,没有卡顿感,我觉得都是好的。而且有些系统内部使用的,即使卡顿一会也无所谓,如果优化的话,用大内存机器成本也在那呢,不用做没必要的优化。

     

     

    • 说说平时工作怎么JVM调优的
      • 如果开发一个新系统,JVM的调优不是一次性就调完的,要分几次去看
        • 第一步,系统开发完需要自己预估一个JVM参数,也就是你预估每秒大概会有多少的对象进入,然后选几台机器,把内存比例设置的合理一些就好了,一般公司都会有一套公司级的通用JVM参数模板,如果是刚开发完,可以直接使用通用模板,反正测试环境还要压测
        • 第二步,测试环境系统压测,使用工具模拟1000人或几千人同时使用,造成每秒几百上千的请求压力,响应时间要控制在200ms。然后压测期间我们需要通过 jstat 去看下内存使用情况,就是我之前说的那些,什么Eden区增长速度,各个GC的频率啊,有没有内存泄漏情况等等吧。如果观察YoungGC和Full GC频率没什么问题,系统没有卡顿现象,就可以上线了。
        • 第三步,如果公司有监控系统,就持续监控,如果没有就每天高峰时期,通过jstat查看一下机器的JVM运行状态,如果需要优化,就继续优化

     

     

     

    展开全文
  • 我们写好代码,是要通过JVM才能运行的 JVM 想要执行一个类,首先要加载类,在加载类之前,需要先编译成字节码class 文件 然后就执行类加载过程,JVM 加载类话,需要类加载器 类加载器是分层级,遵循双亲...

     

    • 我们写好的代码,是要通过JVM才能运行的
    • JVM 想要执行一个类,首先要加载类,在加载类之前,需要先编译成字节码class 文件
    • 然后就执行类的加载过程,JVM 加载类的话,需要类加载器
    • 类加载器是分层级的,遵循双亲委派机制,
      • 最上层是Bootstrap ClassLoder,加载java的核心类库,加载java安装目录下的lib目录的class文件
      • 第二层是Ext ClassLoder,加载一些java的其他类库,加载java安装目录下的lib/ext目录下的class
      • 第三层是Application ClassLoder ,应该程序类加载器,这个类加载器是加载我们写的类
      • 如果我们自定义类加载器的话,那就是第四层
      • 类加载器遵循双亲委派机制,就是说,如果要加载一个类,先去为他的父类能不能加载,如果父类上面还有父类,就继续问,直到顶层。然后顶层说加载不了,就下派到子类,如果所有父类都加载不了,那就自己加载。这么做的好处是,不会重复加载一个类
    • 然后说一下类加载的过程,分这么几步,加载,验证,准备,解析,初始化。 
      • 加载的话,就是刚才说的类加载器去加载类
      • 验证阶段,主要是验证加载的字节码是否符合JVM规范,不然随便瞎写JVM也执行不了
      • 准备阶段,主要是给对象申请内存,然后给变量设置初始值,该设置0的设置0,该设置null的设置null
      • 解析阶段,主要是给符号引用变成直接引用,就是把一些变量什么temp,直接换成物理地址,不然执行的时候JVM也不认识temp是啥
      • 初始化阶段:主要是给变量赋值,准备阶段只是设置了初始值,这个是核心阶段,执行类的初始化,如果发现这个类的父类没有初始化,会先暂停,然后去初始化父类,也是走类加载的一套流程,直到父类加载完了,再执行子类的初始化
    • 这是类加载的过程,加载的类是放到了JVM的元数据空间,也就是永久代。

     

     

     

    • 永久代的话,我们JVM参数一般会给设置个256M,这个一定要设置,绝对不能不设置JVM参数,使用默认JVM参数可能就给新生代分配一两百兆,永久代分配的可能也很少,一旦并发量上来,系统扛不住,永久代一般就放点类和常量池,一般给256M够了,如果给小了,可能导致频繁的Full GC,因为永久代如果满了,会触发Full GC,这个是很坑的
    • 类加载到永久代后,会把类交给字节码执行引擎去执行,画图
    • 执行这个操作是线程去执行的,每个线程都配有一个程序计数器Java虚拟机栈

     

    • 因为Java是支持多线程的,所以必须要有程序计数器记录这个线程执行到哪了
    • Java虚拟机栈在执行每个方法的时候,都会创建一个栈帧,main方法也一样
    • 局部变量都放到这个栈帧中,如果这个方法执行完了,局部变量也就失效了
    • 这里的栈帧如果没有执行完时,其实都是GC Root,垃圾回收时,就是根据这里的局部变量的引用和永久代的引用来判断对象是否存活
    • 我们设置JVM参数的时候,一般都会给Java虚拟机栈 1M的大小,一个系统运行最多几百个线程,不用设置太大,浪费内存,也不能设置太小,容易溢出,特别是递归调用的时候
    • 然后局部变量保存的都是对象的地址,地址指向了JVM堆内存

     

    • 如果是使用ParNew + CMS 垃圾回收器的话,堆内存分年轻代老年代是很明确的,不像G1
    • 然后说一下ParNew垃圾回收器
      • 这个垃圾回收器是回收年轻代的,使用的是多线程回收,不像之前的Serial回收器使用单线程回收。
      • 然后ParNew使用的是复制清除算法,把年轻代分为Eden区 和两个Survivor,JVM 参数默认的占比是 8:1:1,系统运行会把对象创建到Eden区,每次YoungGC 会标记存活对象复制到Survivor0中,再次YoungGC时,再把存活对象复制到Survivor1中。系统运行期间会保证一直有一个Survivor是空着的
      • Eden区的占比有时是可以调优的,如果条件有限,没有大内存的机器,然后对象创建的还特别频繁,存活的对象比较多,那就建议把Eden区比例调低一些,让Survivor大一点,宁可Young GC多一些,也不要让Survivor触发了动态年龄审核或者放不下存活对象。如果放不下那就把这批对象扔到老年代了,Full GC是很慢的。如果是调低Eden,YoungGC会很频繁,但是YoungGC特别快,我通过jstat 看,回收100M垃圾大概也就1ms,所以,如果内存实在不够,降低Eden去比例也不是不可以。但是如果有条件的话最好的话还是加大新生代内存,毕竟YoungGC也是要Stop the World的。
    • 然后继续说ParNew ,它非常适合回收年轻代内存。因为年轻代一般存活的对象是很少的,大多数都是刚创建出1毫秒就变成了垃圾,所以把极少数存活的对象标记出来,复制成本还是很低的,如果像老年代那样采用标记清除算法,那就太慢了

     

    • 然后说一下老年代的垃圾回收器CMS,这个垃圾回收器是使用的 标记-清除 + 整理 算法,我们一般在JVM参数会指定这算法和整理的频率,JVM参数默认是,标记-清除,5次之后,才会去整理内存空间,让对象整齐排列。但是这个默认参数不太好,这样做会有大量的内存碎片,如果某一次从年轻代晋升一个大对象,老年代居然找不到一块连续的内存,就会触发Full GC,那就坑了。我们会把那个值调成0,就是每次CMS垃圾回收后,都会整理内存,虽然每次的回收时间会多一些,但是不会出现内存碎片。

     

    • CMS 垃圾回收分为4个步骤
      • 第一步是初始标记:初始标记的话,只标记GC root直接引用的对象,只有很少一部分,这个阶段需要STW,但是影响不大,这个过程特别快。这个过程也可以优化,JVM 有个参数是初始标记阶段多线程标记,减少STW时间,正常是单线程标记的。
      • 第二步是并发标记:这个阶段是不需要STW的,是和系统并行的处理,系统继续运行,然后垃圾回收线程去追踪第一步标记的GC root,这一步是很耗时的,但是不影响程序执行。因为在垃圾回收时是允许系统继续创建对象的,所以这个过程会有新的对象进来,也会有标记存活的但是现在变成垃圾,这些有改动的对象JVM都会记下来,等待下一步处理。这一步有一个缺点,并发清理时也有这个问题,就是会占用CPU资源。如果是一个4核的机器,那会占用一个CPU去垃圾回收,公式是(cpu核数 + 3)/4。所以一般CPU资源负载特别高的时候,就俩情况,要不是程序的线程太多了。要不就是频繁FullGC,导致的。
      • 第三步是重新标记:重新标记阶段,会把并发标记阶段有改动的对象重新标记,这一步需要STW,不过也是比较快的,因为改动的对象不会特别多,但是要比第一步慢因为要重新判断找个对象是否GC可达。这里也可以通过JVM参数优化,可以通过参数控制,让CMS在重新标记阶段之前尽量触发一次Young GC(尽量YoungGC是因为可能新生代可能刚刚YoungGC不久,那此时就没必要再一次YoungGC了)这样做的好处是,改动的对象中从存活变为垃圾的那部分,就被清理掉了,缩短STW时间。虽然YoungGC也会造成停顿,但是YoungGC一般频率是比较快的,早晚都要执行,现在执行一举两得。
      • 第四步是并发清理,并发清理是和系统并行的,不需要STW。这个阶段是清理前几个阶段标记好的垃圾。
      • 最后,我们通过JVM参数设置,每次Old GC后都重新整理内存,整理阶段会把老年代零零散散的对象排列到一起,减少内存碎片。
    • 在说ParNew + CMS调优之前,我们先说下JVM的几种GC,Young GC,Old GC,Full GC
      • Young GC 和 Old GC 我上面都已经说过了
      • 再说下 Full GC 吧,Full GC就是全面回收整个堆内存,包括新生代、老年代、永久带。整个过程极其的慢,一定要减少Full GC的次数,一般出现频繁的Full GC有几种情况,我们要避免出现这几种情况
        • 第一种是,内存分配不合理,导致Survivor放不下,或者触发了动态年龄审核机制,频繁的往老年代放对象。
        • 第二种,有内存泄漏问题,导致老年代大部分空间被占用,回收都回收不掉,导致每次新生代晋升一点点对象,就放不下了,触发Full GC
        • 第三种,大对象,一般是代码层面的问题,创建了太多的大对象,大对象是直接放入老年代的, 大对象过多会导致频繁触发Full GC
        • 第四种,永久代满了,触发Full GC,我们JVM参数设置256M基本够了,如果不出现代码层面的bug ,一般不会出现这种情况
        • 第五种,有人在代码里误调用了System.gc(),写了这个方法后,如果有机会,JVM就会发生一次Full GC。不过JVM参数可以禁止这种情况,不允许主动调用,我们要加上
    • 一般什么情况下我们要警觉是不是频繁的Full GC了
      • 第一种情况,CPU负载折线上升,特别高
      • 第二种情况,系统卡死了,或者系统处理请求极慢
      • 第三种情况,如果公司有监控系统,会报警。。
    • 然后再说一下ParNew + CMS 调优的问题吧。如果一个系统需要JVM调优,那其实说白了就是Stop the World 太久了,导致系统太卡了。我们说的调优,其实就是减少STW的时间,让系统没有明显的卡顿现象。
    • 然后分析下,需要STW的有几个地方。YoungGC,和Old GC的两个阶段。但是YoungGC一般STW时间特别短,Old GC时间一般会是Young GC的几倍到几十倍,而且占用CPU资源严重。所以,我们优化的重点是让系统减少Old GC的次数。最好让系统只有YoungGC,没有Old GC,更没有Full GC
    • 所以,优化的重点就是尽量不要让对象进入老年代。如果对象进不去老年代,想Full GC都难。这是JVM调优的重点,对象进入老年代的情况也有几种
      • 第一种,对象经过15次YoungGC,依然是存活的,那晋升老年代
        • 这个其实我们是可以优化一下的,因为如果系统1分钟或者30秒一次YoungGC,那没必要非得让对象存活十几分钟才进入老年代,一般存活个两三分钟,这个对象大概率就是要存活很久的了。所以,我们当时是调低了这个参数的,设置了5。不然这个对象一直存活,然后在两个Survivor里来回复制,如果这个对象小一点还好,如果这个对象挺大的,那容易触发Survivor的动态年龄审核机制,让一大批对象进入老年代。所以,该进入老年代的对象,就让他赶紧进去。
      • 第二种,Young GC后存活的对象大小超过Survivor 的50%,那就会触发动态年龄审核机制,如:1岁、2岁、3岁、4岁的对象加起来大于Survivor 的50%,那大于等于4岁的对象全部进入老年代。
      • 第三种,Young GC后存活的对象大于Survivor的大小,那这一批对象直接全部进入老年代,特别坑。
      • 第四种,大对象直接进入老年代,这个JVM参数里是可以设置的,一般我们都设置1M,大于1M的对象进入老年代,一般很少有1M的对象,一般都是个大数组,或者map。
    • 第一种情况和第四种情况,一般是可控的。所以想要优化的话,主要是要在Survivor的大小这块下功夫。我们要避免动态年龄审核和Survivor放不下的情况。要想保证这点,我们就要知道,我们系统的高峰时期,JVM中每秒有多少对象新增,每次YoungGC存活了多少对象。这就需要用 jstat 了。
    • 首先要使用 jstat -gc PID 1000 1000
      • 找到JVM的PID,然后每秒打印一次JVM的内存情况,如果系统访问量比较小,每秒的增长不是很明显,那就把每次的间隔时间调大一点,比如一分钟打印一次
      • 通过这行命令,我们可以看到当时的内存使用情况,有几个列比较重要的数据
      • S0C:Survivor0 的大小
      • S1C:Survivor1 的大小
      • S0U:Survivor0 使用了多少
      • S1U:Survivor1 使用了多少
      • EC:Eden 区的大小
      • EU:Eden 区使用了多少
      • OC:老年代的大小
      • OU:老年代使用了多少
      • MC:永久代的大小
      • MU:永久代使用了多少
      • YGC:YoungGC次数
      • YGCT:YoungGC的总耗时
      • FGC:Full GC次数
      • FGCT:Full GC的总耗时
    • 一般使用 jstat 优化,重点观察这几个指标
      • Eden 区对象的增长速度
        • 上面的几列,通过一行数据是看不出来Eden 区每秒增长多少数据的,所以我们才每秒打印一次,通过上一秒和下一秒EU的数据就可以推断出每秒增长了多少。这个数据进来多打印几行,取个平均值。
      • Young GC 频率
        • 我们我们知道系统启动时间,用YGC的大小除也能算,但是谁没事记得系统什么时候启动的。而且如果我想看高峰时期某一段时间的呢,就看不了了。看几十天的平均值也没什么意义。所以这个高峰时段YoungGC的频率是通过,Eden的大小,除以Eden区对象的增长速度来算的,Eden区对象增长速度,我们已经知道了。
      • Young GC 耗时
        • 这个YoungGC耗时,我们取平均值就行,用YGCT除以YCG,时间除以次数就是每次的耗时。如果说就像看高峰时段的,因为CPU等使用率比较高,可能会影响回收时间,也可以单独看几次的YoungGC,算出时间。
      • Young GC 后多少对象存活
        • 这个指标还是比较重要的,我们要确定每次存活的对象Survovir到底能不能放得下。我们要保证每次存活的对象要小于Survivor的50%,否则就会触发动态年龄审核机制。
      • 老年代对象增量速度
        • 老年代对象增长速度,决定了Old GC的频率。发生Old GC后,FGC那一列也会增长,FGC那一列其实是FullGC 和Old GC的总和。经过优化后的JVM,每次YoungGC不应该进入太多的对象,不进入或者每次进入几兆是比较好的。这个指标我们也要分多次观察,因为只看一次YoungGC晋升的大小是片面的。我们现在已经知道了YoungGC的频率,如果是3分钟一次,那我们就3分钟打印一次内存情况。jstat -gc PID 180000 100,取多次晋升大小的平均值就行。如果晋升的对象特别多,我们需要分析这些对象为什么会进入老年代,上面我说了有四种情况会晋升老年代,到底是哪种情况。是Survivor不够大,还是大对象太多了,或者有内存泄漏导致对象回收不掉,进入了老年代。这个还要具体分析一下的。如果是Survivor太小,我们很轻易就能看出来,如果每次Young GC后S区都是0,那说明存活的对象太多,S区放不下,都进入了老年代。如果S区不是0,有一部分,但是每次回收进入老年代都很多,就有可能是触发动态年龄审核,这个最好再通过GC日志看一下,通过JVM参数可以让系统打印每次GC的日志。如果出现内存泄漏,数据一般是这样的,发现每次FGC次数加1后,老年代并没有多少数据被回收掉,占用了很多。这就大概率是内存泄漏,导致老年代回收不掉。如果是大对象,数据会这样显示,发现及时没有Young GC,OU也会一直在涨,因为大对象是不用经过年轻代的直接进入老年代。如果内存泄漏和大对象的情况,我们可以用 jmap 打印一份内存快照,用MAT工具分析一下到底是什么对象特别大,通过分析出来的堆栈信息就可以定位到代码的位置。
      • Full GC 频率多高
        • 看这个频率和看YoungGC的频率是一样的,可以看高峰时期某几次的平均值。这个Full GC是很耗时的,Full GC的频率我们最好控制在一天1次或者几天一次的范围。特别是对时效性要求比较高的系统,一定要减少Full GC次数。
      • 一次Full GC 的耗时
        • 这个可以取平均值,也可以取某一段的。我们会发现这个Full GC的耗时是YoungGC的好多倍
    • ParNew + CMS 的原理和优化大概就是这么样的,下面我说一下现在比较流行的G1回收器
    • 有人说G1比ParNew +CMS好,可以全面取代,没必要用ParNew和CMS了,我觉得不是这样的。G1有G1的优点,但是也有缺点。我们选择垃圾回收器还是要根据系统的实际情况来看。但是ParNew+CMS的确有不足的地方,如果某些系统使用的机器是大内存,16G、32G,那每次GC都要等Eden区放满了才执行垃圾回收,一次回收好几G的垃圾,那太慢了,可能停顿时间几十上百毫秒,Full GC甚至要几秒。那就太坑了,不可以接收。这个时候就必须用G1了。
    • 那什么情况下用ParNew+CMS呢,它又什么优点
      • 它的优点就是我们可以优化到极致,极致到没有Full GC只有YoungGC。但是G1不行,我们对G1的优化只能是尽可能的优化预定的停顿时间,其他的我们没法参与太多,因为它什么时候YoungGC我们都不确定。
      • G1的内存使用率是没有ParNew +CMS高的,G1有这么一个机制,如果G1的某一个Region存活对象达到了85%,那就不会去回收这个Region,但是那15%呢。如果是垃圾也回收不掉了。
      • G1的掌控性没有ParNew + CMS好,说白了就是心里没底。我们使用ParNew + CMS可以很确定多久YoungGC,对象增长速度等等等等吧,我们都能看到。但是G1什么时候垃圾回收我们都不知道,如果出现了内存泄漏,如果不是几个G的内存泄漏,我们也很难察觉出来。使用ParNew + CMS可以放心一些,不用搞个活动心惊胆战的。
    • 所以总结来说,如果是4核8G的机器,尽量还是用ParNew + CMS垃圾回收器,如果是大内存机器,就是用G1。
    • 然后说一下G1的原理吧,G1把堆内存平均分成了多个大小相同的Region,我们首先要设置堆内存的大小,然后G1会根据堆大小除以2048,分成2048个大小相同的Region。G1也是有年轻代、老年代的概念,但是只是概念。没有ParNew+CMS分的那么清楚。G1里的年轻代和老年代都是基于Region的,某些Region属于年轻代,某些Region属于老年代,由G1动态控制。但是现在属于年轻代的Region并不永远都是年轻代,如果年轻代的Region被回收了,下次这个Region可能就存放老年代的数据了。所以,G1的年轻代和老年代都是动态的,但是也有个上限。系统刚开始运行时,会给年轻代分配5%的Region来存放对象,年轻代最多可以占用60%的Region,这60%可以通过JVM参数指定,默认是60%,不过这个一般默认就好,如果达到了目标值,就会强制触发YoungGC
    • G1的年轻代也是分Eden和Survivor的,因为G1整体使用的都是复制回收算法。只是某些Region属于Eden,某些Region属于Survivor,系统新创建的对象会被分配到属于Eden的Region,如果垃圾回收就把存活对象复制到Survivor中。
    • G1的一个特点就是我们可以设置一个预期的停顿时间,也就是STW的时间,比如,某个系统的时效性要就特别高,每次GC我只允许STW的5ms,那我们就可以通过JVM参数设置成5ms的停顿,这样G1在垃圾回收时,就会把时间控制在5ms以内
    • G1的垃圾回收不一定是年轻代满了,或者老年代满了才去回收。如果是那样,就和ParNew+CMS没区别了,大内存机器也要STW好久。G1是基于每个Region的性价比去回收的,比如,Region1里有20M对象,回收2ms,Region2里有50兆对象回收要4ms。如果我们设置系统停顿时间为5ms,那G1会在要求的时间内,尽可能回收更多的对象,它会选择Region2,因为性价比更高。所以,我们系统运行,一直往Eden放对象,如果G1觉得,此时回收一下垃圾,差不多要5ms,那可能G1就回去回收,不会等到年轻代占用60%才去回收。
    • G1中年轻代的对象什么情况下会进入老年代
      • 其实和ParNew + CMS整体上是差不多的,只有大对象的处理不一样
      • 1、YoungGC存活的对象Survivor放不下
      • 2、YoungGC存活的对象达到Survivor的50%,触发动态年龄审核
      • 3、对象到达了15岁,进入老年代
      • G1中大对象不会进入老年代,而是专门有一部分Region存放大对象用。如果一个Region放不下大对象,那就会横跨几个Region来存放。
    • G1的Old G也不是我们能控制的,G1会根据自己的判断觉得该回收的时候就会回收,不过也是基于复制算法的
    • G1的混合回收,如果老年代占比45%,就会触发混合回收,回收整个堆内存,但是混合回收也是会控制在我们设置的停顿时间的范围内的,如果时间不够,就会分多次回收。混合回收有点和CMS的回收类似
      • 第一步,初始标记
        • 初始标记需要STW,这一步只标记GC Root直接应用的对象,速度很快
      • 第二步,并发标记
        • 和系统并行,深入的追踪GC Root,标记所有存活的对象,此时系统新创建的对象会被JVM记录,这一步不需要STW
      • 第三步,重新标记,重新标记第二步有改动的对象,要STW。因为只有一小部分改动,速度很快
      • 第四步,混合回收,只有这一步和CMS不一样,CMS这里的回收时和系统并行的。但是G1的混合回收需要STW。混合回收不仅会回收老年代,还会回收新生代和大对象。如果一次性全回收掉,那时间就太久了,可能达不到我们设置的预期停顿时间,所以G1这里是分几批来回收的,回收一次,系统运行一会,然后再回收一次。JVM参数可以设置这个值,分几次去回收,默认值是8次,分8次回收。混合回收还有一个参数我们可以设置,就是空闲的Region达到百分之多少,停止回收,默认是5%
    • G1何时会触发Full GC,其实G1的混合回收就相当于ParNew + CMS的Full GC了,因为回收了所有的区域,只不过回收时间可以控制在我们指定的范围内。但是G1的Full GC就没法控制了,可能要卡顿特别久才能回收完。什么情况下会出现呢,因为G1的整体是基于复制算法的,如果回收的过程中,发现存活对象找不到可以复制的Region,放不下了。那就Full GC,开始单线程标记、清理、整理空闲出一批Region,这个过程很慢
    • 然后说一下G1的优化,G1比较智能,我们可以参与优化的点很少,我们只能合理的设置停顿时间,不要太小也不要太大,太小GC会太频繁,每秒都在GC。太大的话,停顿时间太久了也不好。
    • 平时我们选择垃圾回收器要根据不同的场景具体去分析,该使用那个。没有绝对的好坏。优化也没有一个统一的标准。比如YoungGC和Full GC多久一次好,YoungGC、Full GC耗时多久比较好。这个还是看系统的,只要不影响系统使用,没有卡顿感,我觉得都是好的。而且有些系统内部使用的,即使卡顿一会也无所谓,如果优化的话,用大内存机器成本也在那呢,不用做没必要的优化。

     

     

    • 说说平时工作怎么JVM调优的
      • 如果开发一个新系统,JVM的调优不是一次性就调完的,要分几次去看
        • 第一步,系统开发完需要自己预估一个JVM参数,也就是你预估每秒大概会有多少的对象进入,然后选几台机器,把内存比例设置的合理一些就好了,一般公司都会有一套公司级的通用JVM参数模板,如果是刚开发完,可以直接使用通用模板,反正测试环境还要压测
        • 第二步,测试环境系统压测,使用工具模拟1000人或几千人同时使用,造成每秒几百上千的请求压力,响应时间要控制在200ms。然后压测期间我们需要通过 jstat 去看下内存使用情况,就是我之前说的那些,什么Eden区增长速度,各个GC的频率啊,有没有内存泄漏情况等等吧。如果观察YoungGC和Full GC频率没什么问题,系统没有卡顿现象,就可以上线了。
        • 第三步,如果公司有监控系统,就持续监控,如果没有就每天高峰时期,通过jstat查看一下机器的JVM运行状态,如果需要优化,就继续优化

     jstat -gc PID 1000 1000

    展开全文
  • JVM工作原理及优化

    2017-10-23 21:32:32
    JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境. 1.创建JVM装载环境和配置 ...在我们运行和调试Java程序的时候,经常会提到一个JVM的概念.JVM是Java程
    JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.

    1.创建JVM装载环境和配置

    2.装载JVM.dll

    3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例

    4.调用JNIEnv实例装载并处理class类。

    在我们运行和调试Java程序的时候,经常会提到一个JVM的概念.JVM是Java程序运行的环境,但是他同时一个操作系统的一个应用程序一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间.

    首先来说一下JVM工作原理中的jdk这个东西,不管你是初学者还是高手,是j2ee程序员还是j2se程序员,jdk总是在帮我们做一些事情.我们在了解Java之前首先大师们会给我们提供说jdk这个东西.它在Java整个体系中充当着什么角色呢?我很惊叹sun大师们设计天才,能把一个如此完整的体系结构化的如此完美.jdk在这个体系中充当一个生产加工中心,产生所有的数据输出,是所有指令和战略的执行中心.本身它提供了Java的完整方案,可以开发目前Java能支持的所有应用和系统程序.这里说一个问题,大家会问,那为什么还有j2me,j2ee这些东西,这两个东西目的很简单,分别用来简化各自领域内的开发和构建过程.jdk除了JVM之外,还有一些核心的API,集成API,用户工具,开发技术,开发工具和API等组成

    好了,废话说了那么多,来点于主题相关的东西吧.JVM在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.

    1.创建JVM装载环境和配置

    2.装载JVM.dll

    3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例

    4.调用JNIEnv实例装载并处理class类。

    一.JVM装入环境,JVM提供的方式是操作系统的动态连接文件.既然是文件那就一个装入路径的问题,Java是怎么找这个路径的呢?当你在调用Java test的时候,操作系统会在path下在你的Java.exe程序,Java.exe就通过下面一个过程来确定JVM的路径和相关的参数配置了.下面基于Windows的实现的分析.

    首先查找jre路径,Java是通过GetApplicationHome api来获得当前的Java.exe绝对路径,c:\j2sdk1.4.2_09\bin\Java.exe,那么它会截取到绝对路径c:\j2sdk1.4.2_09\,判断c:\j2sdk1.4.2_09\bin\Java.dll文件是否存在,如果存在就把c:\j2sdk1.4.2_09\作为jre路径,如果不存在则判断c:\j2sdk1.4.2_09\jre\bin\Java.dll是否存在,如果存在这c:\j2sdk1.4.2_09\jre作为jre路径.如果不存在调用GetPublicJREHome查HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\“当前JRE版本号”\JavaHome的路径为jre路径。

    然后装载JVM.cfg文件JRE路径+\lib+\ARCH(CPU构架)+\JVM.cfgARCH(CPU构架)的判断是通过Java_md.c中GetArch函数判断的,该函数中windows平台只有两种情况:WIN64的‘ia64’,其他情况都为‘i386’。以我的为例:C:\j2sdk1.4.2_09\jre\lib\i386\JVM.cfg.主要的内容如下:

        -client KNOWN   
        -server KNOWN   
        -hotspot ALIASED_TO -client   
        -classic WARN   
        -native ERROR   
        -green ERROR  

    在我们的jdk目录中jre\bin\server和jre\bin\client都有JVM.dll文件存在,而Java正是通过JVM.cfg配置文件来管理这些不同版本的JVM.dll的.通过文件我们可以定义目前jdk中支持那些JVM,前面部分(client)是JVM名称,后面是参数,KNOWN表示JVM存在,ALIASED_TO表示给别的JVM取一个别名,WARN表示不存在时找一个JVM替代,ERROR表示不存在抛出异常.在运行Java XXX是,Java.exe会通过CheckJVMType来检查当前的JVM类型,Java可以通过两种参数的方式来指定具体的JVM类型,一种按照JVM.cfg文件中的JVM名称指定,第二种方法是直接指定,它们执行的方法分别是“Java -J”、“Java -XXaltJVM=”或“Java -J-XXaltJVM=”。如果是第一种参数传递方式,CheckJVMType函数会取参数‘-J’后面的JVM名称,然后从已知的JVM配置参数中查找如果找到同名的则去掉该JVM名称前的‘-’直接返回该值;而第二种方法,会直接返回“-XXaltJVM=”或“-J-XXaltJVM=”后面的JVM类型名称;如果在运行Java时未指定上面两种方法中的任一一种参数,CheckJVMType会取配置文件中第一个配置中的JVM名称,去掉名称前面的‘-’返回该值。CheckJVMType函数的这个返回值会在下面的函数中汇同jre路径组合成JVM.dll的绝对路径。如果没有指定这会使用JVM.cfg中第一个定义的JVM.可以通过set _Java_LAUNCHER_DEBUG=1在控制台上测试.

    最后获得JVM.dll的路径,JRE路径+\bin+\JVM类型字符串+\JVM.dll就是JVM的文件路径了,但是如果在调用Java程序时用-XXaltJVM=参数指定的路径path,就直接用path+\JVM.dll文件做为JVM.dll的文件路径.

    二:装载JVM.dll

    通过第一步已经找到了JVM的路径,Java通过LoadJavaVM来装入JVM.dll文件.装入工作很简单就是调用Windows API函数:

    LoadLibrary装载JVM.dll动态连接库.然后把JVM.dll中的导出函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVM和GetDefaultJavaVMInitArgs函数指针变量上。JVM.dll的装载工作宣告完成。

    三:初始化JVM,获得本地调用接口,这样就可以在Java中调用JVM的函数了.调用InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例.

    四:运行Java程序.

    Java程序有两种方式一种是jar包,一种是class. 运行jar,Java -jar XXX.jar运行的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用Java类Java.util.jar.JarFileJNIEnv中方法getManifest()并从返回的Manifest对象中取getAttributes("Main-Class")的值即jar包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。main函数直接调用Java.c中LoadClass方法装载该类。如果是执行class方法。main函数直接调用Java.c中LoadClass方法装载该类。

    然后main函数调用JNIEnv实例的GetStaticMethodID方法查找装载的class主类中

    “public static void main(String[] args)”方法,并判断该方法是否为public方法,然后调用JNIEnv实例的

    CallStaticVoidMethod方法调用该Java类的main方法。  
    展开全文
  • jvm原理及优化

    2018-10-28 21:18:51
    jvm的启动: 装载配置 => 加载类 => 执行主方法 由类加载器加载类文件到内存,包括堆,栈,方法区以及本地方法区等等,,, 方法区保存类的信息(常量池,字段方法等信息等等) 堆包含了应用程序中的...
  • jvm原理及java性能优化

    2020-05-29 10:03:47
    1.Java源文件,通过编译器,产生.Class字节码文件,字节码文件通过Java虚拟机中的解释器,编译成特定机器上的机器码简单来说:通过类加载器加载字节码文件,被分配到JVM的运行时数据区的字节码会被执行引擎执行。...
  • Jvm运行原理

    2018-09-10 13:12:17
    但是由于JVM对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的专门做性能优化的什么的),很少有人能很好的去学习理解什么是JVM,以及弄清楚JVM的工作原理,个人认为这块还是非常有...
  • JVM运行原理

    2018-08-14 15:01:00
    但是由于JVM对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的专门做性能优化的什么的),很少有人能很好的去学习理解什么是JVM,以及弄清楚JVM的工作原理,个人认为这块还是非常有...
  • 本章聊聊Tomcat如何进行...Tomcat调优主要内容如下:1、增加最大连接数2、调整工作模式3、启用gzip压缩4、调整JVM内存大小5、作为Web时,动静分离6、合理选择垃圾回收算法7、尽量使用较新JDK版本生产环境Tomcat配...
  • 1. 默认运行模式 ...【基础+实战】JVM原理及优化系列之一:JVM体系结构 【基础+实战】JVM原理及优化系列之二:JVM内存管理 【基础+实战】JVM原理及优化系列之三:JVM垃圾收集器 【基础+实战】JVM原理及优化...
  • JVM原理优化及GC

    2017-06-12 15:17:11
    JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.1....在我们运行和调试Java程序的时候,经常会提到一个JVM的概念.JVM是Java程序运行的环境,但是他同时一个操作系统的一个
  • 详解JVM运行原理

    2018-08-30 15:25:01
    但是由于JVM对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的专门做性能优化的什么的),很少有人能很好的去学习理解什么是JVM,以及弄清楚JVM的工作原理,个人认为这块还是非常有...
  • Jvm简介jvm执行原理概述1.Java源文件,通过编译器,产生.Class字节码文件,字节码文件通过Java虚拟机中的解释器,编译成特定机器上的机器码简单来说:通过类加载器加载字节码文件,被分配到JVM的运行时数据区的字节...
  • 1、标准参数(-),所有JVM都必须支持这些参数功能,而且向后兼容;例如: -client——设置JVM使用Client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位...
  • 1. JVM主要由两个子系统和两个组件组成: ...1、运行时数据区:JVM的内存区域 2、本地接口:即JNI,一个标准的java API,支持异构语言集成 2. 体系结构图: 附加说明: 1、线程隔离区:指的是以...
  • 1.运行模式  2.内存分配优化  3.GC 调优  4.线程调优生产环境 2. JVM配置原则 1.编译器:启动服务器编译器,可以提高效率。  2.口志输出:开启口志输出,用于跟踪 GC 信息,方便后续进一步优化配置  3...
  • 但是由于JVM对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的专门做性能优化的什么的),很少有人能很好的去学习理解什么是JVM,以及弄清楚JVM的工作原理,个人认为这块还是非常有...
  •  在$JAVA_HOME/jre/bin下有client和server两个目录,分别代表JVM的两种运行模式。  client运行模式,针对桌面应用,加载速度比server模式快10%,而运行速度为server模式的10分之一。client下默认的堆容量 -Xms1M ...
  • 在本课程中个,将详细介绍JVM的基本原理、组成以及工作方式,并配合实际案例,介绍相关的调优技巧。 课程大纲: 第一课 初识JVM JVM分类 Java语言规范 JVM规范 介绍JVM的基本知识和发展历史,并介绍了Java语言...
  • 但是由于JVM对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的专门做性能优化的什么的),很少有人能很好的去学习理解什么是JVM,以及弄清楚JVM的工作原理,个人认为这块还是非常有...
  • jvm分析及优化

    2019-06-13 16:39:59
    JVM的初次见面,是在我们Java SE课程的开始,讲解Java跨平台原理的时候.时隔多日,我们先来回顾一下. Java的广告语是,”编写一次,到处运行”,而它凭借的就是JVM(Java Virtual Machine).而对于不同的平台,Windows,...
  • 但是由于JVM对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的专门做性能优化的什么的),很少有人能很好的去学习理解什么是JVM,以及弄清楚JVM的工作原理,个人认为这块还是非常有...
  • JVM 原理优化 (1)

    2019-01-25 09:00:18
    1、体系结构生命周期 如图所示,JVM包括类装载器子系统、运行时数据区、执行引擎。类装载器子系统根据给定权限 名来装入类型(类或者接口)。执行引擎负责执行那些包含在被装载类方法中指令。运行时 数据...
  • 本文介绍了JVM平台上CPU Profiler实现原理,希望能帮助读者在使用类似工具同时也能清楚其内部技术实现。 引言 研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈。...
  • 本文介绍了JVM平台上CPU Profiler实现原理,希望能帮助读者在使用类似工具同时也能清楚其内部技术实现。 引言研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈。Profiling...

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 149
精华内容 59
关键字:

jvm的运行原理及优化