JVM知识总结

JVM知识总结

一、JVM的介绍

跨平台执行

  • JVM是一种规范,也是Java能跨平台运行的基础
  • JVM有很多实现,像HotSpot,Jrocket(曾号称世界上最快的JVM,后被Oracle公司收购,合并于HotSpot中),J9,TaobaoVM,zing(zual公司开发的很牛的非开源商业产品,很贵但是回收速度非常快,其内垃圾回收算法被HotSpot吸收后才有了ZGC)。
  • 同一种实现在不同平台的实现细节是不一样的,就像Windows系统的HotSpot和Linux的HotSpot就不一样,所以才能根据同一份class文件转换成各自平台能够识别并执行的指令,所以各种语言只要按照JVM的规定把自己语言编译成JVM规定的特定格式的class文件就能在JVM上运行,也就能实现跨平台运行

二、Java类的运转过程

在这里插入图片描述

  • 一个java文件,首先经过Java编译器编译成class文件,然后由类加载器加载到JVM内存中,然后对class文件格式进行验证,给静态成员变量等附默认值,解析,然后初始化静态成员变量等,然后就可以使用这个类了。

  • 类加载过程 loading
    a.这里先了解一下几种类加载器:自定义类加载器Custom classLoader,其父加载器为应用类加载器App classLoader,其父加载器为扩展类加载器Extendsion classLoader,其父加载器为启动类加载器Bootstrap classLoader。
    b.各个类加载器负责加载类的路径不一样,自己只负责加载自己负责路径下的类
    c.如果要加载一个class文件A,首先应用类加载器会去内存查找该类有没有被加载过了,如果有,那就返回类,如果没有并不会立刻加载,会先去询问一下它的父类加载器扩展类加载器有没有加载过这个文件了,如果有就返回,如果没有,就会去询问一下它的父类加载器启动类加载器有没有加载过这个文件了,如果有就返回,如果没有,就会去它负责的路径下尝试加载该类,由于它负责路径下找不到该类,就会委托它的子类加载器扩展类加载器去加载该类,扩展类加载器会去它负责的路径下尝试加载该类,由于它负责路径下也找不到该类,就会委托它的子类加载器应用类加载器去加载该类,在其负责路径下找到类就加载返回,找不到就抛classNotFound异常。上面的流程,也就是双亲委派机制流程(如下图)。目的主要是为了防止核心类被篡改带来的安全问题,其次是为了防止类重复加载,加快效率。
    在这里插入图片描述

    d.但有时候会需要去打破双亲委派机制达到某种功能,比如Tomcat的热部署等。那么如何打破双亲委派机制呢?只需要自定义类加载器,重写loadclass方法即可,观看其源码,可以知道双亲委派机制整个过程是写死在loadclass里面了,所以重写loadclass方法即可打破双亲委派机制

  • 连接 linking
    连接这块又细分为验证,准备,解析三个小部分
    a.验证 验证加载进来的class文件是否符合JVMclass文件的格式,比如是否以cafe babe开头等
    b.准备 为类的静态变量分配内存和赋默认值(如int类型变量就赋值0)
    c.解析 将常量池中的符号引用替换为直接引用过程

  • 初始化 initialing
    初始化是将静态变量赋初始值的过程,比如static int i=7,这一步才是真正的将7赋值给了i

  • 使用(new一个对象过程)
    首先根据对象申明找到对应的class,看class文件有没有被加载到内存了,如果没有,就进行上述类加载,连接,初始化过程,然后创建对象,为对象开辟内存空间,然后赋默认值给成员变量,然后调用构造方法,同时初始化成员变量,最后赋值对象地址给对象变量。如下图
    在这里插入图片描述

三、对象的内存布局

在这里插入图片描述

对象在内存的分布分为三大部分,对象头,实例数据和对齐。

  • 对象头
    对象头里面主要分为markword,类指针和数组长度(如果是数组对象的话)
    • markword
      在这里插入图片描述
      markword占用8字节(这里针对64位计算机,下同),它里面的内容会随着对象的状态改变而改变,主要是无锁,偏向锁,轻量级锁,重量级锁,GC几种状态,如上图。这里需要注意一下分代年龄是用4bit来记录了,所以最大年龄只能记录到15,所以分代年龄设置为大于15是不正确的。
      这里顺便讲一下synchronized锁升级过程:
      首先是无锁状态,markword里面存储了锁对象的hashcode,非偏向锁状态,锁标志位01;假如此时有一个线程A过来要获取锁,由于没有其他线程,所以就是偏向锁状态,记录了A线程的ID,偏向时间,同时为偏向锁状态,锁标志不变;过了一会,又有一个线程B过来获取锁,由于偏向锁是设计为有竞争的时候才会释放,所以不管A有没有完成它要执行的指令,该锁此时还是相当于在A手上,所以B过来获取锁的时候,就是发生了锁竞争了,此时会判断A是否处于存活状态,如果非存活状态,那么锁对象就变成无锁状态,B可以继续拿到锁,把里面偏向所线程ID改为B,如果A是处于存活状态,那么就会升级为轻量级锁,锁标志位00,;此时多个线程竞争锁其实就是CAS修改指向自己栈中的锁记录的指针(每个线程会在栈中复制一份无锁对象),修改失败的话,就会自旋,自旋到一定次数后,锁就会膨胀为重量级锁,锁标志位10;此时锁竞争,就是竞争monitor对象,进入竞争等待队列,竞争成功为owner,其他的就在队列里等待唤醒,等owner释放锁就会唤醒其他线程竞争锁。至此锁升级完毕,该过程是不可逆的。

    • 类指针
      指向类class文件。建立对象是为了使用对象,我们程序需要通过栈上的引用来操作堆上的具体对象,由于引用在JVM规范里面只规定了一个指向对象的引用,并没有定义这个引用以什么方式去定位访问堆中的对象的具体位置,所以对象的访问方式也是不一样的,目前主流的方式有两种:句柄和直接指针

    • 句柄池其实就是引用指向该类的句柄,句柄里记录着类指针和堆对象指针,所以寻找对象其实是先访问句柄,然后再根据对象指针访问对象,如图1。

    • 直接指针其实就是引用直接指向堆对象,对象里面存储了类的指针,如需使用类对象,根据对象里面的类指针去寻找,如图2

      这两种方式都有自己的优点缺点,句柄方式比直接指针多了一层指针指引过程,但是指针改变比直接指针小,直接指针每次都需要修改引用的值。HotSpot里面用的是直接指针方式,所以我们这里讲的类指针就是指的这个,类指针在默认情况下(开启类指针压缩),占用4个字节
      在这里插入图片描述
      在这里插入图片描述

    • 数组长度
      当对象是数组对象时,会比普通对象多一个数组长度的数据,占用4字节,记录着数组的长度。

  • 实例数据
    这里存储的是对象里定义的变量。
  • 对齐
    HotSpot规定对象大小必须是8字节的倍数,所以当对象头大小和实例数据大小达不到这个要求时,就需要使用对齐来占用字节,达到平台要求。就是起着占用符的作用。
  • 一个经典面试题(默认参数下Object o = new Object()占用多大内存?)
    64位计算机下:16字节:对象头8字节,类指针4字节,对齐补齐4字节

四、JVM内存模型

JVM会在程序执行时,将其管理的内存划分为几个不同的数据区,各司其职,划分大概包括以下几个运行时数据区:程序计数器,本机方法栈,虚拟机栈,堆,方法区
在这里插入图片描述

  • 程序计数器(独享)
    当前线程字节码行号指示器,线程的恢复,循环,跳转等都需要依赖它来完成。比如A线程执行代码到第五行了,此时A线程被CPU挂起,去执行B线程,后续继续执行A的时候,难道代码要从头执行?当然不是,拿到计数器所记录的行号,往下继续执行
  • 虚拟机栈(独享)
    记录着一个方法从开始到结束期间的数据,称为栈帧。一个方法对应一个栈帧,所以虚拟机栈里面就是一个个栈帧。一个栈帧又包括局部变量表,操作数栈,方法出口,动态链接;操作数栈是一个中间结果存放栈,每次运算都是将局部变量表的数据压入栈,由栈弹出给CPU运算后,再压入栈,然后再弹出修改局部变量表;方法出口,是指方法A里面调用了方法B,B方法执行完成后,该从哪里开始继续执行方法A
  • 本地方法栈(独享)
    本地方法栈和虚拟机栈差不多,区别在于,虚拟机栈为虚拟机执行我们写的方法,本地方法栈为虚拟机执行native方法。
  • 方法区(共享)
    方法区是一个逻辑概念,存放着类信息,常量,静态变量,运行时常量池等数据。JDK1.7之前它的具体实现叫做永久代Perm space,1.7之后叫做元数据区meta space,其区别为元数据区把它里面的字符串常量池挪到了堆中。
  • 堆(共享)(一个面试题:堆都是共享的?)
    a.堆是JVM管理内存中最大的一块,用于存放对象,几乎所有对象都要在这里分配内存;这里为什么说是几乎所有呢,因为随着JIT编译器和逃逸分析技术的发展,对象也可以栈上分配内存了。
    b.随着内存大小的变化和垃圾回收器的升级而变化,像起初内存是逻辑分代,物理也分代;到了G1垃圾回收器的时候,就是逻辑分代,但是物理没有分代了;后面到了ZGC的时候,就没有分代这个概念了。下面就讲一下前期那种情况:
    堆分为年轻代和老年代,默认比例是1:2,年轻代又分为Eden区,s0,s1区,比例是8:1:1
  • 对象创建过程以及存活运转过程
    new一个对象的时候,首先尝试往栈上分配,分配不下,如果是大对象(大对象是多大?参数可以设定),直接往老年代分配,如果不是大对象,先尝试往堆上的TLAB上分配(为了避免使用指针碰撞方式在堆上分配内存时带来的线程不安全问题,在堆上为每个线程开辟了自己的线程本地分配缓存ThreadLocalAllocationBuffer,简称TLAB),TLAB用完了,就会往Eden区上分配。当一次GC过后,存活对象会往S0或S1区挪动,同时分代年龄+1,当分代年龄大于设定的分代年龄时(默认15),对象会被挪到老年代中;或者当S0或者S1区中同一年龄段的对象占比超过一半时,年龄大于或等于该年龄的对象直接晋升到老年代,这过程叫动态年龄判定;或者当发生YGC前,JVM会先检查老年代的内存连续空间是否大于年轻代所有对象的大小,如果不大于,就会判断参数HandlePromotionFailure是否设置为允许担保失败,如果允许,会继续检查老年代的连续内存空间是否大于以往晋升到老年代对象的平均大小,如果大于,就可以进行YGC,如果不大于或者参数不允许担保失败,就改为一次FullGC。 在这里插入图片描述

五、垃圾回收算法

  • 垃圾定位
    有两种办法定位垃圾:引用计数法,可达性分析
    • 引用计数:给对象添加一个计数器,有人引用它,计数器加1,引用失效,计数器减1。当计数器为0时,为垃圾,但是很难解决循环引用问题
    • 可达性分析:通过从GC Roots对象开始沿着引用链向下搜索,当搜索完全部引用链,也没有找到一个对象的话,那么该对象就是垃圾
      什么对象可以称为GC Roots对象呢?
      本地方法栈中引用的对象;方法区中静态属性引用的对象;方法区中常用引用的对象;Native方法引用的对象
  • 垃圾回收算法
    找到垃圾后就要通过垃圾回收算法进行垃圾的回收了,垃圾回收算法主要分为这么几种:标记清除,复制,标记整理
    • 标记清除:循环一遍,做好标记,将垃圾清除掉。缺点:会产生内存碎
    • 复制:将内存一份为二,每次只用其中一部分,将存活的对象拷贝到另外一份中。缺点:造成内存的浪费,每次只能用一部分;优点是快,不会产生内存碎片
    • 标记整理:将存活对象移动到垃圾对象前面。优点:不会产生内存碎片;缺点:效率低,涉及到对象的大量挪动。
    • 分代算法:根据不同内存区域内对象存活时间的不同,采用不同回收算法,提高效率;一般由于年轻代对象存活时间短,采用复制算法,而老年代对象一般存活时间长,垃圾对象相对较少,故采用标记整理或者标记清除

六、垃圾回收器

基于上面的垃圾回收算法,多种垃圾回收器就被开发出来,真正执行垃圾的回收。常见的回收器组合大概分为这么几类:单线程的,多线程的,PN+CMS,G1,ZGC

  • 单线程:Serial+Serial Old 是单线程的,串行的执行垃圾回收
    在这里插入图片描述
  • 多线程:Parallel Scavenge+Parallel Old(PS+PO)
    在这里插入图片描述
  • PN+CMS PN是Parallel New,年轻代垃圾回收器的一种,是PS的增强版,为了适配CMS。CMS垃圾回收器是一种跨时代的垃圾回收器,因为在它之前垃圾回收线程和工作线程是串行的,它的出现使得两者并行运行,减少了STW停顿时间。CMS采用的是标记清除垃圾回收算法,其垃圾回收过程分为四步:初始标记,并发标记,重新标记,并发清理
    • 初始标记只是标记GC Roots对象,会发生STW,但时间很短,并发标记过程是从GC Roots对象开始沿着引用链遍历的过程,非常耗时,所以这里能跟工作线程并发执行,大大缩短了垃圾回收时间,重新标记是对在并发标记里工作线程改动的对象进行重新标记,并发清理阶段清理垃圾,但工作线程还在工作,也会产生浮动垃圾,只能等到下次垃圾回收的时候再来清理这部分垃圾了。
      在这里插入图片描述
    • 这里CMS用的是三色标记算法,实现并发标记。下面来了解一下三色标记算法:黑色表示对象自己已被标记以及其引用的对象被标记;灰色表示对象自己被标记了但其引用对象还没被标记;白色表示对象和引用对象都还没被标记,如图。
    • 但是这过程会有漏标的情况,比如:在并发标记期间,A由于自己和其引用对象都被标记了,所以是黑色了,B由于其引用D还没有被标记,所以B为灰色,D为白色,假如此时B去掉D的引用,A指向了D,如图二,此时由于A已经是黑色了,对于黑色的对象,是不会再沿着其引用链往下遍历了,然后从B开始沿着引用链往下遍历也找不到D了,此时D就会很尴尬,没被标记到,也就是漏标情况。对于没标记到的对象(白色),会认为它是垃圾,可能会将其回收掉,但是此时D还被A引用着喔,很显然不是垃圾,不应该被回收。
    • 如何解决这个问题呢?有两种方式:增量更新和SATB。增量更新是指,当黑色对象A有新增引用时,将A变成灰色,后续重新标记的时候,就能沿着引用链找到D了;SATB,全称是SnapshotAtTheBegining,就是是删除对象引用的时候做个引用快照,存到一张列表里面,后续只要遍历这张表就可以找到D了。两种方法当然是SATB更好,因为增量更新,还得重新遍历一遍A中其他已经标记过的引用对象,CMS就是用的增量更新,G1用的SATB。

在这里插入图片描述
在这里插入图片描述

  • CMS垃圾回收器优缺点
    优点:CMS是第一个使得垃圾回收和工作线程可以部分并发运行的垃圾回收器,停顿时间较短
    缺点:1.对CPU资源比较敏感,因为是垃圾回收线程和工作线程并发运行,会抢占工作线程CPU资源,降低用户执行速度
    2.会产生浮动垃圾,可能会出现Concurrent Model Failure。前面也说过了并发清理的时候,工作线程有可能会产生垃圾,而这些垃圾只能在下次执行垃圾回收时回收了。正是由于垃圾回收过程,工作线程也要工作,加上浮动垃圾所占空间的影响,所以要提前留出一部分空间给工作线程工作,也就是说,使用CMS情况下,FullGC不能等到老年代满了才触发,所以有个参数CMSInitiatingOccupancyFraction来指定老年代使用了多少百分比了就触发FullGC。但是这里也带来了个问题,设置多少合适,如果设置太小,那么FullGC频率就太高了;如果设置太大,当CMS垃圾回收过程空间不够的情况,会出现Concurrent Model Failure,也就是并发模式失败,这时就会临时启动Serial Old垃圾回收器来进行垃圾回收,但由于Serial Old是单线程的,会使得停顿时间变得更长了。
    3.由于CMS采用的是标记清除回收算法,所以就会产生内存碎片,当空间碎片过多的时候,会给大对象分配内存带来很大麻烦,往往会出现明明老年代内存还很多却不得不触发一次FullGC,为解决该问题,提供了一个参数:UseCMSCompactAtFullCollection,用于进行FullGC的时候,CMS垃圾回收器开启对老年代内存碎片的合并整理,但整理碎片过程不是并发的,会使得停顿时间变长,怎么办呢,只能是通过参数CMSFullGCsBeforeCompaction,来设置多少次不带整理碎片的FullGC后来一次带整理碎片的FullGC来缓和该问题

  • G1(标记整理+复制算法)
    G1开发出来的使命就是能够替换掉CMS,因为CMS确实还有很多缺点,虽然是跨时代的部分并发回收垃圾 。
    G1和CMS一样,也是可以部分并发回收垃圾,而且回收过程也差不多,所用算法也是三色算法。
    G1和CMS不同的地方在于:1.由于物理区分老年代和年轻代,CMS只负责老年代的垃圾回收,PN负责年轻代的垃圾回收;而到了G1,JVM内存模型就不在物理区分老年代和年轻代了,虽然还有这个概念,JVM内存被分为一个个相等的Region,每个Region在经过多次垃圾回收后,可能当过年轻代,老年代,或者大对象区域。
    在这里插入图片描述

    2.CMS采用的是标记清理方法,而G1从整体来看是标记整理方法,从两个Region来看是复制算法,两者都避免了产生内存碎片,算是解决了CMS的一个缺点。3.G1可以预测停顿时间,用户可以根据参数,规定一次垃圾回收停顿要在多少时间内(但是不一样能够达到,只能说是努力达到)。G1是通过跟踪监测每个Region的垃圾回收价值,也就是回收后所获得的空间大小和回收所需要的时间的经验值,每次回收后都会将回收价值记录在一个优先列表里面,这个列表叫做Collection Set(CSet),然后每次回收都会根据CSet优先回收价值最大的Region,加上动态调整其年轻代所占堆大小比例(默认5%-60%内来回调整)来达到设定的停顿时间。
    3.在前面也说过,在垃圾回收过程,会从GC Roots对象开始沿着引用链往下遍历对象,其实这时候有个问题是,如果GC Roots对象在老年代,那么每次YGC的时候,都要遍历老年代的对象来查找有没有对象指向年轻代的对象好做判断是否回收对象?这样做的话每次YGC涉及的范围就比较大了,所以就有了card table,卡表。卡表将堆划分为一个个卡页(类比物理内存的page),当卡页上的对象有其他引用时,会改变该卡页上的标记位,称之为dirty card。在G1时,还额外为每个Region维护一个Remembered Set(RSet)来记录本Region对象被其他Region对象引用的信息,当虚拟机在对引用类型数据进行写操作时,也就是赋值动作时,会产生一个Write Barrier(写屏障)暂停写操作,检查被引用的对象与其是否处于不同的Region中(或者是否在不同年龄代中),如果是,就通卡表把相关引用信息,记录到被引用对象所属的RSet中。

  • ZGC

  • Shenandoah

七、硬件内存模型

在这里插入图片描述

  • 起初CPU之间都是共用主存,但随着CPU的发展,CPU运算速度越来越快,跟内存的速度已经有好几个量级的差别了,所以为了提高CPU的利用率(避免执行一个指令等待很长的内存IO时间),在CPU和内存之间设置了高速缓存(L1,L2,L3,越靠近CPU速度越快,同时越贵,其中L1和L2是CPU独享的,L3是CPU共享的),然后使用的时候会将主存的数据拷贝进去高速缓存中(当然这里并不是拷贝所有,下面会将拷拷贝哪里,怎么拷贝),相当于CPU之后只需要跟L1拿数据,速度是相当的,就提高了CPU的利用率了,但是,这又带来了另外一个问题,怎么保证缓存的一致性呢?正是由于每个CPU都会从主存中拷贝一份数据到自己的高速缓存中,一个CPU一旦改变数据,怎么同步到其他CPU的缓存中呢?
  • 现代计算机是通过缓存一致性协议+总线锁来保证数据一致性的。
    缓存一致性协议有很多种,这里以Intel的MESI为例介绍其怎么保证数据一致性的
    在这里插入图片描述
    MESI协议里面定义了缓存行有四种状态:修改,独享,共享,失效,分别对应字母MESI。CPUA读取缓存行X时,由于还没有其他CPU访问过,此时CPUA中的缓存行状态为独享;过了一会CPUB也来访问X,发现有人访问过该缓存行了,就通知CPUA修改其状态为共享,CPUB中的缓存行状态也为共享;过了一会,CPUB修改了自己缓存中X中的数据,那么CPUB中的X的状态为modify,此时CPUA的X的还是共享,当CPUB要将X写回主存时,主存发现要写回的X的状态是modify,于是通知其他拥有X的CPU修改他们的X的状态为失效,等其他CPU修改好后,才将X写回主存;过了一会,CPUA想要操作自己缓存中X中的数据,发现X的状态是失效,就会重新去主存上读取新的X到自己缓存中。
  • 这里了解一下主存的数据是怎么拷贝进高速缓存的
    CPU读取某个数据的时候,访问L1,L1没有就从L2读,L2没有就从L3读,L3没有就从主存读,读到数据,依次拷贝进L3,L2,L1。但并不是用到哪个数据就拷贝哪个数据,这样效率太低,而是将跟读取的数据在同一行的数据一起拷贝进高速缓存中,这样一行的数据也叫缓存行,CPU就是以缓存行为单位读取数据的,大小为64字节。但是缓存行也会产生为伪共享问题:假如一个缓存行中有数据A和B,CPUA和CPUB的高速缓存中都有该缓存行,CPUA只需要操作数据A,CPUB只需要操作数据B,在该场景下,假如CPUB不断操作数据B,CPUA就需要不断更新自己的缓存,但是CPUA其实是不需要关注数据B的,也就是数据B的变化其实CPUA是不关心的,它只会操作数据A。这就是伪共享带来的效率问题。可以用缓存行对齐来解决该问题,上述问题,只要数据A和数据B位于不同缓存行,是不是就没有上面的问题了?是的,所以如何将A和B分开呢?我们知道一个缓存行是64字节,只需要在AB前后各自补充几个多余变量来填充缓存行的大小到64字节即可将A和B分开在不同缓存行了

八、Java内存模型(JMM)

  • 在之前,C/C++直接使用硬件的内存模型作为自己的内存模型,会出现由于不同平台上内存模型的差异造成一套程序在一套平台上完全正常运行,而到了别的平台就会出错的问题,只能针对不同平台定制化开发。因此Java定义了自己的内存模型,它是在硬件内存模型基础上更高层的抽象和增强,屏蔽了各种硬件和系统对内存访问的差异性,以实现Java程序在任何平台下都能达到一致的内存访问效果。
  • JVM内部每个线程都有自己的工作内存,进程内所有线程共享主内存,线程并不会直接对主内存上的数据直接操作,而是将数据拷贝进自己的工作内存,对数据的操作也是在自己的工作内存里面完成的,线程之间的工作内存不能直接访问。所以多线程并发操作同一个数据的时候,就会有缓存不一致的问题,也就是可见性问题。像synchronized,volatile都可以保证可见性。保证可见性的底层原理其实就是MESI协议,但MESI协议并没有规定CPU里面修改的数据啥时候写回主存,而配合synchronized和volatile会让数据在修改后立即写回主存。
    在这里插入图片描述

九、 乱序执行和禁止乱序执行

除了增加高速缓存,在符合条件下CPU或者即时编译器(JIT)会对指令进行重排序,来达到CPU的更高利用。但指令重排序的前提是保证单线程下指令乱序执行结果不变。但有时候乱序执行会带来一些问题,所以提供了volatile禁止指令重排序,保证其有序性。另外synchronized也能保证有序性。
待续:了解synchronized和volatile其可见性和有序性底层原理(monienter moniexit lock前缀指令)。

十、JVM调优

  • JVM常用调优参数

    • GC常用参数
      -Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间
      -XX:+UseTLAB 使用TLAB,默认打开
      -XX:+PrintTLAB 打印TLAB的使用情况
      -XX:TLABSize 设置TLAB大小
      -XX:+DisableExplictGC 使System.gc()引起的FGC不管用
      -XX:+PrintGC
      -XX:+PrintGCDetails
      -XX:+PrintHeapAtGC
      -XX:+PrintGCTimeStamps
      -XX:+PrintGCApplicationConcurrentTime 打印应用程序时间
      -XX:+PrintGCApplicationStoppedTime 打印暂停时长
      -XX:+PrintReferenceGC 记录回收了多少种不同引用类型的引用
      -verbose:class 类加载详细过程
      -XX:+PrintVMOptions
      -XX:+PrintFlagsFinal -XX:+PrintFlagsInitial 查看JVM的参数
      -Xloggc:opt/log/gc.log 指定gc日志
      -XX:MaxTenuringThreshold 升代年龄,最大值15
      锁自旋次数 -XX:PreBlockSpin 热点代码检测参数-

    • Parallel常用参数
      -XX:SurvivorRatio
      -XX:PreTenureSizeThreshold 大对象到底多大
      -XX:MaxTenuringThreshold
      -XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
      -XX:+UseAdaptiveSizePolicy 自动选择各区大小比例

    • CMS常用参数
      -XX:+UseConcMarkSweepGC
      -XX:ParallelCMSThreadsCMS线程数量
      -XX:CMSInitiatingOccupancyFraction
      使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
      -XX:+UseCMSCompactAtFullCollection在FGC时进行压缩
      -XX:CMSFullGCsBeforeCompaction多少次FGC之后进行压缩
      -XX:+CMSClassUnloadingEnabled
      -XX:CMSInitiatingPermOccupancyFraction达到什么比例时进行方法区回收
      GCTimeRatio 设置GC时间占用程序运行时间的百分比
      -XX:MaxGCPauseMillis 停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代

    • G1常用参数
      -XX:+UseG1GC
      -XX:MaxGCPauseMillis
      建议值,G1会尝试调整Young区的块数来达到这个值
      -XX:GCPauseIntervalMillis GC的间隔时间
      -XX:+G1HeapRegionSize
      分区大小,建议逐渐增大该值,1 2 4 8 16 32。
      随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
      G1NewSizePercent 新生代最小比例,默认为5%
      G1MaxNewSizePercent 新生代最大比例,默认为60%
      GCTimeRatio
      GC时间建议比例,G1会根据这个值调整堆空间
      ConcGCThreads 线程数量
      InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例

  • JVM调优监控常用工具和命令
    jmap -histo pid |head -20 查看占内存最高的前20个对象
    jmap -dump:file=xxx pid 堆转储文件
    jhat xx.hprof:分析堆转储文件
    jstack pid:查看进程内线程快照
    top:查看所有进程cpu,内存使用情况等
    top -Hp pid:查看某个进程内线程cpu,内存使用情况等
    图形化界面工具:jvisiualVm,jconsole,jprofiler
    线上排查工具:arthas
    jps:查看java进程
    jinfo:查看JVM参数

  • CPU飙高问题排查
    a.先top命令找到cpu最高的进程A pid为11238
    在这里插入图片描述

    b.top -Hp pid 查看A进程里面线程CPU使用情况
    在这里插入图片描述

    c.jstack pid 查看A进程里面线程的情况
    在这里插入图片描述

    d.分析c步骤的文件(是垃圾回收线程占比高还是工作线程占比高;哪个方法比较耗时;是否发生了死锁等),比如b步骤查到A线程CPU比较高,可以在c步骤查出来的线程快照里查找该线程的情况,注意要将b中的pid转为16进制再去搜,这里我查找的是11257的线程,转为16进制就是2bf9
    在这里插入图片描述
    在这里插入图片描述

  • 内存溢出问题排查
    a.jmap -histo pid |head -20 查看占内存最高的前20个对象
    在这里插入图片描述

    b.jmap -dump:file=xxx pid 将堆情况dump下来,配合jhat或者用远程图像工具jvisualvm等分析堆内对象情况,定位占内存最大对象产生原因(对象存活时间过长;内存泄漏;死循环创建对象;等),执行jhat之后,会有个端口7000,页面访问一下7000,或者用jvisualvm装入堆转储文件,就可以查看里面具体情况了。这里要注意的是,线上dump要谨慎,如果是高可用情况下,可以先摘掉那个节点,不然dump期间可能会产生卡顿。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 图形监控工具
    a. 想要使用远程工具的情况下,要在服务启动的时候添加启动参数 -Djava.rmi.server.hostname=192.168.17.11 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11111 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false ,启动后,打开jconsole或者jvisualvm,用指定的端口(这里是11111,上面参数指定的)连接上去
    b. 如果不能用远程工具的情况下,用arthas感觉是不错的,这个是在线监控工具,用法也简单,下载下来,然后java -jar启动,指定java进程,就可以用arthas命令在线观察这个进程内的情况了

  • 内存溢出几种情况
    a.堆内存溢出:一般是不断生成新对象,存活时间又很长。会报错OutOfmemoryException:Java heap space
    b.栈内存溢出:一般是由于递归调用方法,栈内存又设置很小,此时会报OutOfmemoryException:stackoverflowError;或者不断创建线程,此时会报OutOfmemoryException:Unable to create new nativethread
    c.方法区溢出:不断动态生成类(代理类),OutOfmemory:meta space或者Perm
    d.本地内存溢出:通过unsafe.allocateMemory或者DirectBuffer直接申请内存,没有释放,OutOfMemoryException—unsafe.allocateMemory

注:才疏学浅,有错误的地方还请各位大佬不吝赐教,留言相告,多谢。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值