撸一把JVM

作者: 涵溢 | 来源:发表于2018-03-23 22:45 被阅读247次
    黄金比例

    数据区

    分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。其中虚拟机栈、本地方法栈、程序计数器是线程私有的,其他是线程共享的。
    程序计数器:当前线程所执行字节码的行号指示器,字节码解释器通过改变这个值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都要依赖这个计数器来完成。每次cpu切换到某个线程去执行都需要恢复到正确的执行位置,所以每条线程都需要维护一个独立的计数器,各个线程互不影响,独立存储。

    虚拟机栈:生命周期与线程相同,每个方法执行都会创建一个栈针,用来存放局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被执行就对应着一个栈针入栈与出栈。

    局部变量表存放了编译期可知的各种基本数据类型、对象引用,局部变量表所需的内存空间在编译期间完成分配,进入一个的方法的时候局部变量表所需的空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

    直接内存:NIO中,引入了一种基于通道与缓冲区的IO方式,使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。在某些场景下中能显著提高性能,因为避免了在java堆和Native堆中来回复制数据。直接内存分配不收到java堆大小的限制,所以一定要预留足够的内存给直接内存使用。


    serial、parNew等待Compact过程的收集器采用的分配算法是指针碰撞;CMS这种基于标记清除的收集器用的是空闲列表。但是仅仅修改一个指针所指向的位置也不是线程安全的,可能正在给对象A分配内存,指针还没来得及修改,对象B又同时指向了原来的指针来分配内存。有2种方式,对内存分配的方式进行同步,实际上虚拟机采用CAS配上失败重试的方式(个人认为有点类似AtomicInteger中的incrementAndGet方法的机制)来保证更新操作的原子性;另一种,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),这样只有在TLAB用完之后分配新的TLAB得时候才需要同步,是否使用TLAB,可以通过-XX:+/UseTLAB参数来指定(默认是开启的)。

    对象头包含:对象哈希码、对象分代年龄和类型指针即对象指向它在方法区里的的类元数据的指针、通过这个指针来确定是哪个类的实例。HotSpot是通过reference“直接指针访问”来定位对象位置的,之所以不是用句柄池(单独划分出的一块内存,reference存储对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息)是因为直接指针访问节省了一次指针定位的时间开销,对象的访问非常频繁就很严重了。

    java堆溢出怎么办?
    先分析是否是内存泄漏:可以通过工具分析堆转储快照文件中的可以对象,分析可以对象到GC Roots的引用链,就可以找到泄漏代码的位置了。
    如果不是内存泄漏:可以设置-Xmx, -Xms, 或者检查是不是对象生命周期过长长时间不能回收。

    -Xss 每个线程的堆栈大小。某些时候需要减少最大堆和减少Xss来换取更多的线程。

    jvm使用的可达性分析算法而不是引用计数来判断对象是否死活(是否要被回收)
    这个算法的基本思路是通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,则证明此对象是不可用的。
    可以作为GC Roots的对象有:

    • 栈帧中的本地变量表中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(Native方法)引用的对象
    引用
    • 强引用:普通的引用
    • 软引用
      用来描述一些还有用但并非必需的对象。SoftReference,发生内存溢出之前进行回收。
    • 弱引用
      WeakReference,只能生存到下次GC之前,GC时回收。

    垃圾收集算法

    • 标记-清除算法:内存碎片、再次分配大对象时如果无法找到连续的内存而不得不得提前触发GC。老年代对象存活率高、没有额外空间对它进行担保,就必须用标记-清除(整理)。
    • 标记-整理算法
    • 复制算法:分块,当某块用完了 ,把活着的对象复制到另一块,把已经使用的内存空间一次清理掉。不用考虑内存碎片 的问题,只需移动栈顶指针按顺序分配内存即可(指针碰撞),实现简单,运行高效。如果另一块Survivor空间没有足够空间存放上次新生代收集下来的存活对象,这些对象直接通过分配担保机制进入老年代。新生代每次只有少量存活,只需要付出少量存活对象的复制成本就可以完成收集。

    垃圾收集器

    ParNew收集器

    复制算法,多线程收集,会stop the world。

    CMS收集器

    标记-清除算法。
    获取停顿时间最短为目标的收集器,分为四个阶段:

    • 初始标记:Stop The World,仅仅是标记下GC Roots能直接关联的对象,速度很快。
    • 并发标记:和用户线程并发执行(可能会交替执行),进行GC Roots Tracing。
    • 重新标记:Stop The World,为了修正并发标记期间因用户程序继续工作而导致标记产生变动的那一部分对象的标记记录,时间长于初始标记小于并发标记。
    • 并发清除:

    耗时最长的并发标记和并发清除阶段是和用户线程一起工作的,从整体来说,CMS收集器的回收过程是与用户线程一起并发执行的。
    并行(Parallel):多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    并发(Concurrent):指用户线程和垃圾收集器线程同时执行(不一定是并行的看,可能会交替进行)。

    直观感受下gc的各个阶段:


    image.png

    CMS的缺点:

    • 并发阶段因为占用了一部分cpu资源,而导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数为:(cpu数目+3)/4 ,核数在4个以上时,大于25%的cpu资源;2个核数时,可能使用户性能降低50%,无法忍受。
    • CMS无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,只好留到下一次GC时再清理掉。因此CMS不能像其他收集器那样等老年代几乎满的时候再收集,需要预留部门空间提供给并发收集时的用户线程使用。默认是92%,可以适当提高以便降低内存回收次数从而提高性能,remote配置:-XX:CMSInitiatingOccupancyFraction=70,当CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Model Failure”失败,这时虚拟机将启动应急方案:临时启用 Serial Old收集器(标记-整理算法),停顿时间就很长了。
    • CMS基于标记-清除,会产生大量空间碎片,过多时,会给大对象分配带来很大的麻烦,往往会出现老年代还有大量的空间,但是找不到足够大的连续空间分配给大对象而不得不提前触发一次Full GC。可以使用-XX:+UseCMSCompactAtFullCollection(默认是开启)用于Full GC时进行碎片的合并整理工作,但是内存整理的过程是无法并发的,空间碎片没问题了,但是时间变长了。还有一个参数:-XX:CMSFullGCsBeforeCompation。

    参数

    • -XX:+UseConcMarkSweepGC, ParNew + CMS + Serial Old
    • -XX:SurvivorRatio=3 , 默认是8, Eden:Survivor
    • -XX:PretenureSizeThreshold=,直接晋升到老年代的对象大小
    • -XX:+HandlePromotionFailure, 是否允许担保失败
    • -XX:ParallelGCThreads=,并行GC时进行内存回收的线程个数。
    • -XX:MaxTenuringThreshold=3(默认是15),对象在Survivor每熬过一次Minor GC年龄就+1,超过次数就晋升到老年代。虚拟机并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。
    • -XX:ParallelGCThreads=2 ,设置并行GC时进行内存回收的线程数。
    • -XX:+PrintGCDetails
    • -Xmx3000m
      是指设定程序启动时占用内存大小
    • -Xms3000m
      是指设定程序运行期间最大可占用的内存大小, 如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
    • -Xmn1g
      设置年轻代大小
      -Xss128k
      设置每个线程的堆栈大小.在相同物理内存下,减小这个值能生成更多的线程。
      -XX:NewRatio=2
      年轻代与老年代比值,即新生代占比1/3
    • -XX:CMSInitiatingOccupancyFraction=70
    • -XX:+UseCMSCompactAtFullCollection
    • -XX:CMSFullGCsBeforeCompation=0
    • -XX:MaxTenuringThreshold=7
    • -XX:MaxPermSize=256M
    • -XX:SurvivorRatio=3
      s0:s1:eden=1:1:3
    • XX:+UseConcMarkSweepGC
    • -XX:-OmitStackTraceInFastThrow
    • XX:+CMSParallelRemarkEnabled
      开启并行remark,多条垃圾收集器线程并行工作。
    • -XX:+CMSScavengeBeforeRemark
      在执行CMS remark之前进行一次youngGC,这样能有效降低remark的时间。
      cms gc会以新生代作为gc root的一部分,因为在remark之前进行一次ygc可以回收大部分对象,从而减少gcroot的开销。如果remark1 time > remark2 time + ygc time就值得开启这个参数。例如:
      开启之前 Remark 0.7s
      开启之后 YGC 0.05s + Remark 0.4s
      参考:https://www.zhihu.com/question/61090975/answer/184878629
    • XX:+AlwaysPreTouch
      这样JVM就会先访问所有分配给它的内存,让操作系统把内存真正的分配给JVM.后续JVM就可以顺畅的访问内存了。
      下图显示的是jvm用了swap,有磁盘io导致gc时间特别长,后来加上AlwaysPreTouch让jvm启动是分配内存而不是运行时慢慢增长内存就好了。
      image.png

    当然也可以使用:cat /proc/66/status 可以看vmSwap,66 是进程号。


    image.png

    参考:http://www.cnblogs.com/rainy-shurun/p/5830455.html

    • -verbose:gc

    和-XX:+PrintGC类似。
    在官方文档中有说明:两者功能一样,都用于垃圾收集时的信息打印。
    -verbose:gc
    稳定版本
    参见:http://docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html
    -XX:+PrintGC
    非稳定版本,可能在未通知的情况下删除,在下面官方文档中是-XX:-PrintGC。
    因为被标记为manageable,所以可以通过如下三种方式修改:
    1、com.sun.management.HotSpotDiagnosticMXBean API
    2、JConsole
    3、jinfo -flag
    参见:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

    • -Xloggc:$logPATH/gc.log
    • PrintTenuringDistribution

    最佳配置

    -Xmx3000m -Xms3000m -verbose:gc -Xloggc:$logPATH/gc.log -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSCompactAtFullCollection -XX:MaxTenuringThreshold=7 -XX:MaxPermSize=256M -XX:SurvivorRatio=3  -XX:NewRatio=2 -XX:+PrintGCDateStamps   -XX:+PrintGCDetails  -XX:+UseConcMarkSweepGC -XX:-OmitStackTraceInFastThrow -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+AlwaysPreTouch
    

    年轻代大小选择
    响应时间优先的应用 :尽可能设大,直到接近系统的最低响应时间限制 (根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

    避免运行时短命大对象的不断产生,要不然新生代来回copy增加gc时间,如果不短命可以设置超过多大的对象直接进入老年代。

    使用swap引起的real时间过长的case

    上图中的user、sys、real与linux的time命令所输出的时间含义一致,分表表示用户态消耗的cpu时间、内核态消耗的cpu时间、操作从开始到结束所经过的墙钟时间。cpu与墙钟时间的区别是:后者包括各种非运算的等待耗时,例如等待磁盘i/o、等待线程阻塞,而cpu时间不含这些。
    多cpu的情况下,多线程操作会叠加这些cpu时间,所以user、sys时间超过real也正常。

    对象优先在Eden分配,当Eden没有足够的空间时,虚拟机将发起一次Minor GC。
    老年代GC(Major / Full GC),会至少伴随一次Minor GC,一半比Minor GC慢十倍以上。

    监控

    jps

    jps:JVM Process(程序) Status
    -m:输出虚拟机进程启动时传递给主类main()方法的参数
    -l : 输出主类的全名
    -v:输出虚拟机进程启动时JVM参数

    jstat

    JVM Statistics

    • -class:类装载、卸载数量、总空间、类装载所耗费的时间


      image.png
    • -gccapacity,-gcnewcapacity,-gcoldcapacity

    • -gcutil:已使用空间占比


      image.png
    • -gccause:与gcutil一样,但是会额外输入导致上一次GC产生的原因

    jinfo -flag CMSInitiatingOccupancyFraction 9477
    jmap
    • jmap -dump:live,file=name pid
      jmap -dump:[live,]format=b,file=<filename> 使用hprof二进制形式,输出jvm的heap内容到文件=. live子选项是可选的,假如指定live选项,那么只输出活的对象到文件.

    • jmap -heap pid


      image.png
    • jmap -histo[:live] pid 查看对象个数

    jstack pid 查看堆栈信息

    相关文章

      网友评论

      • 涵溢:避免运行时短命大对象的不断产生,要不然新生代来回copy增加gc时间,如果不短命可以设置超过多大的对象直接进入老年代

      本文标题:撸一把JVM

      本文链接:https://www.haomeiwen.com/subject/hxutcftx.html