JVM与GC

2020-06-01 00:00:00 并行 执行 对象 内存 回收

近处理客户高并发应用的时候,经常会遇到GC的问题,于是是时候把原理学习过的JVM和GC的内容拿出来温习一下了。


一些准备


-XX:+<option>启用该项如:-XX:+UseSerialGC使用串行回收


-XX:-<option>不启用该项如:-XX:-UseAdaptiveSizePolicy不使用自适应的Eden、S0、S1大小调节


-XX:<option>=<value>给选项设定值如:-XX:SurviorRatio=8设置Eden与Survivor区的大小比例


-Xmx:HeapDumpPath=./dump.core堆内存快照存储区



JVM标准结构



类的加载机制


一:装载(load)

         由ClassLoader负责加载; (ClassNotFoundException)

二:链接(Link)

         校验(verify)、准备(Prepare)、初始化静态变量赋默认值;

                (NoClassDefFoundError)

三:初始化

         执行静态初始化代码、赋值静态变量。


ClassLoader


一:BootStrap ClassLoader

               由Sun用C++实现此类,JDK启动时负责加载jre/lib/rt.jar里的所有

               class,及java规范中定义的接口及实现;

二:Extension ClassLoader

        JVM用于加载扩展功能的jar包,如DNS.jar

               Jdk中的名称:sun.misc.Launcher$ExtClassLoader

三:System ClassLoader

               加载启动参数中指定ClassPath的Jar包

               Jdk中的名称: sun.misc.Launcher$AppClassLoader

              一般的程序使用的都是AppClassLoader

四:自定义ClassLoader

               用户也可以自定ClassLoader用于加载其他路径的Jar如网络上加载



类的执行机制


虽然类已加载成功且静态属性及实例对象皆以创建,但是,执行静态方法或者实例方法时仍需要对JVM字节码进行处理。


JVM有以下三种执行方式:

(1):解释执行

(2):编译执行

(3):反射执行


解释执行


采用经典的冯诺依曼FDX循环方式,即获取下一条指令,解码并分派,然后执行。


解释执行的优点:

简单、占资源少、启动速度快


解释执行的缺点:

效率低




编译执行

JDK将字节码编译成机器码,编译在运行时进行,故称作JIT编译器(Just-in-time)•策略:对执行频率频繁的代码使用编译执行,对执行频率不高的仍采用解释执行


编译执行的两种方式:

(1)ClinetCompiler:又称C1较轻量级,java  -client

(2)ServerCompiler:  又称C2 较重量级,java -server


Client Compiler


占用内存较少,适合于桌面应用•优化方法:

   一:方法内联,将调用的方法指令直接植入到当前方法中;

   二:去虚拟化,针对只有一个实现类的方法;

   三:消除冗余,在编译时进行代码清理;


Server Compiler


C2采用了大量优化技巧,占内存较多,适合于服务器应用;

优化方法:标量替换、栈上分配、同步削除;

默认当CPU个数超过两个且内存超过2G自动采用Server模式,否则为Client模式,但在32位的windows上始终都是Client模式,可通过java-server强制使用Server模式,或者java-client强制使用client模式。


JVM内存管理


JVM内存结构图:



JVM方法区


用于存放类信息、类的属性、方法等信息。


又称为持久代PermanentGeneration,默认小值16MB,大值64MB。持久代在一定条件下也会被GC(垃圾回收),当空间不够时,会抛出OutOfMemory错误信息。


可通过-XX:PermSize  -XX:MaxPermSize来指定小大值。


PC寄存器与方法栈


每个线程均会创建自己的PC寄存器和方法栈。

PC寄存器存放每条指令的地址。

方法栈为线程私有,当方法执行完毕时,其栈帧所用内存会自动被回收。

当栈空间不足时会抛出StackOverflowError,可通过设置-Xss来指定其大小,以避免空间不够用。


JVM堆


堆用于存储对象实例和数组值。

在32位机上大2GB,在64位机则无限制

可通过-Xms设置小值,默认为物理内存的1/64但小于1GG

通过-Xmx设置大值,默认为物理内存的1/4但小于1GB

默认当空余堆内存小于40%时,JVM会增大Heap到大值,可通过-XX:MinHeapFreeRatio=来设定这个比例。

当空余空间大于70%时,JVM会减小到小值,可通过-XX:MaxHeapFreeRatio=来设定这个比例。

为避免运行时JVM频繁调整Heap大小,通常将-Xms与-Xmx设成相同值。



JVM堆结构




新生代 new generation


  • 大多数情况下Java创建的对象都从新生代分配。

  • 新生代由Eden Space和两块大小相同的Survivor Space(又称S0和S1或者From和To)构成。

  • 可通过-Xmn指定新生代的大小。

  • -XX:SurvivorRatio来调整Eden和Survivor的大小比例。


旧生代 Old Generation


  • 用于存放在新生代中多次回收仍然存活的对象。

  • 有两种情况新建的对象会直接在老生代分配:

    • 通过设置-XX:PretenureSizeThreshold,当新对象大小超过设定值时直接在老生代分配,但是当新生代采用Parallel Scavenge GC时该设置。另一种是大的数组对象,且数组中无引用外部对象。

  • 旧生代的大小为-Xmx减去-Xmn的值。


内存回收-GC


  • 所谓内存回收就是我们所熟知的GC(Garbage Collection)垃圾回收。

  • JVM通过GC来回收堆和方法区的内存,GC的原理为首先找到内存中不再被引用的对象,然后回收。

  • 通常采用收集器的方式来实现GC,主要的收集器有引用计数收集器和跟踪收集器。


引用计数收集器


引用计数收集器通过记录对象的引用次数,当次数为零时,可进行回收。

但当出现循环引用时该收集方法则无法回收:



所以引用计数方式不适合面向对象这种有复杂引用关系的语言,SunJDK在实现GC时也未使用过此方式。


跟踪收集器

  • 跟踪收集器采用集中式的管理方式,全局记录数据的引用状态。基于一定的条件触发例如:空间不足,定时。

  • 执行时需要从根对象来扫描对象间的引用关系,这会造成应用的暂停。

  • 主要实现算法有:赋值(Copying)、标记-清除(Mark-Sweep)、标记-压缩(Mark-Compact)。


复制算法-Copying


复制算法采用的方式为从根集合扫描出存活的对象,并将存活的对象复制到一块新的完全未使用的空间。



当存活对象少时,Copying算法是很高效的,其代价是需要一块新的存储区和进行对象移动。


标记-清除 Mark-Sweep


  • 标记-清除采用的方式为从根集合开始扫描,对存活的对象进行标记,标记完成后,对未被标记的对象进行扫描并回收。

  • Mark-Sweep不需进行对象移动,且仅对不存活的对象进行处理。在空间存活对象较多的情况下较为高效,但会形成内存碎片。




标记-压缩 Mark-Compact


  • 标记-压缩与标记-清除的不同在于,对回收的内存空间进行压缩,即所有存活的对象都往左端空闲的空间进行移动,并更新引用对象的指针。

  • 该算法的好处在于不产生内存碎片,减少OutOfMemory的风险,缺点是移动对象的成本较高。



新生代可用GC


  • 新生代的大多数对象存活时间较短,故采用了Copying算法。

  • 新生代的GC又叫Minor GC。

  • Minor GC有三种回收方式:串行GC(Serial GC)、并行回收GC(Parallel Scavenge)、并行GC(ParNew)。


串行GC(Serial GC)


  • 串行GC从根集合扫描存活的对象。JVM认为根对象为当前线程栈上引用的对象、常量、静态变量、传到本地方法还未被释放的引用。

  • 串行GC扫描存活的对象,然后将存活的对象复制到S0或S1中(S0与S1同时只能有一个被使用,另一个为空)。

  • 为了避免扫描过程中引用关系的改变,JDK采用了暂停应用的方式。

  • 通常只有经过几次MinorGC仍存活的对象才放入旧生代中。该次数可通过-XX:MaxTenuringThreshold设置(只在串行与ParNew方式下生效默认值为15)。但该项并不是的规则。串行和ParNew在每次GC后计算可存活的次数,规则为累计每个age对象占用的内存,如果累计超过SurvivorSpace的一半,则以age为准,否则,以MaxTenuringThreshold为准。

  • 如果ToSpace空间满则直接转入旧生代。




SerialGC采用单线程方式,适用于单CPU,对暂停时间要求不高的应用,也是Client级别(CPU小于2个或物理内存小于2GB,或32位Windows机器上)的GC方式,可通过-XX:+UseSerialGC来强制执行。


并行回收GC(Parallel Scavenge)


  • 当采用PSGC时,默认ServivorRatio比使用-XX:InitialSurvivorRatio来设置,如果不设置该项,则默认为6:1。

  • 一般情况下PSGC会自动调整Survivor比例,可通过-XX:-UseAdaptiveSizePolicy来固定Survivor比例。

  • PSGC不是根据-XX:PretenureSizeThreshold来决定对象是否直接在旧生代分配,而是当,EdenSpace空间不够的情况下,而此对象的大小大于等于EdenSpace一半的大小,则直接在旧生代分配(该情况应该比较少见)。

  • 并行回收适合多CPU、对暂停时间要求较短的应用。是Server级别的默认GC方式,也可通过-XX:+UseParallelGC来强制指定。

  • 默认的执行线程数与CPU的核数相同,但当CPU核数大于8时,其计算公式为:3+(CPU核数*5)/8,也可通过-XX:ParallelGCThreads来强制指定线程数。


并行GC(ParNew)


  • 在SurvivorRatio分配上与串行GC的策略一样

  • 并行GC的不同之处在于须配合旧生代的CMS GC使用,由于CMS是并发进行的,若此时发生MinorGC需要做相应的处理。

  • 当采用CMSGC时,新生代默认采用并行GC,也可使用+XX:+UseParNewGC来强制指定。


旧生代与持久代GC


  • JDK提供三种旧生代GC方式:串行、并行、并发。

  • 串行基于Mark-Sweep-Compact实现,首先从根集合对象扫描,然后按照三色着色算法标识对象;扫描未标识的对象并将其回收;执行滑动压缩,将存活的对象向旧生代的开始处滑动,后留出一块连续的到结尾处的空间。


旧生代串行GC


  • 串行GC是Client级别机器和32位Windows机器上采用的方式,可通过-XX:+UseSerialGC来强制指定;

  • 串行的整个执行过程需要暂停应用,且是单线程进行,通常会花费很长时间,可通过-XX:+PrintGCApplicationStoppedTime来查看GC造成的应用停止时间。


旧生代并行Compacting


  • 旧生代的并行Compacting通过以下三步来完成:

    • 首先将旧生代分为并行线程个数个区regions并行地进行着色;

    • 然后从左边扫描regions,直到找到个值得进行压缩的region,并将此region左边的region作为高密区(dense prefix),对这些区域不进行回收,继续向右扫描,找到需要进行压缩的源region和目标region。此过程为单线程进行。

    • 后基于regions上的分析,并行地进行压缩和region回收。

  • 较之串行并行大部分时间是并行进行,故造成的应用暂停时间会缩短。

  • 并行GC是Server级别机器上默认采用的GC方式,可通过-XX:+UseParallelOldGC指定使用ParallelCompacting, -XX:+UseParallelGC指定Parallel-Mark-Sweep。









并发(CMS:Concurrent Mark-Sweep)


CMS主要采用Mark-Sweep方式,因为会产生较多内存碎片,故不能用bump-the-pointer分配,而采用free list分配。


Free List方式导致MinorGC速度下降。


CMSGC的回收过程为以下四步:


1.次标记(Initial Making)

该步骤暂停整个应用,从根集合到旧生代扫描可直接访问的对象,并着色,将着色的对象用一个外部的bit数组进行记录。

2.并发标记(Concurrent Making)

该步骤恢复所有应用,对着色过的对象进行轮询,标记这些对象可访问的对象。

为解决MinorGC造成的旧生代引用关系的改变,CMS采用Mod Union Table记录每次MinorGC后修改的Card信息。(旧生代的根对象)

采用Card Table记录与应用并发时的dirty对象

3.重新标记(Final Marking)

该步骤暂停整个应用,主要任务是对ConcurrentSweeping时新建的对象及Mod Union Table和Card Table中的对象进行扫描,并重新着色。

4.并发收集(Concurrent Sweeping)

恢复所有应用线程,将没有标记的对象回收,回收时,CMS会将相邻的两个区域合并成一个新的大的区域。

为避免CMS与应用争抢CPU资源,CMS提供了一种增量模式,i-CMS。






开启CMS

  • 默认JVM并不启动CMS,可通过-XX:+UseConcMarkSweepGC来启动。

  • Perm代的CMS启动参数为-XX:+CMSClassUnloadingEnabled

  • 默认的线程数为(并行GC线程数+3)/4,也可通过-XX:ParallelCMSThreads= 指定。


CMS的触发条件

  • 当旧生代已使用的空间达到CMSInitiatingOccupancyFraction或CMSInitiatingPermOccupancyFraction设定的百分比时CMS会触发,JDK6中该值默认为92。

  • 另一种方式为JVM自动触发,JVM基于GC的频率及旧生代的增长趋势来评估何时触发CMS。

  • 若不希望JVM自动触发,可设定-XX:UseCMSInitiatingOccupancyOnly=true


CMS的问题

  • CMS机制会产生内存碎片,降低了内存使用率,同时增加了OutOfMemory的可能性。

  • -XX:+UseCMSCompactAtFullCollection来启动CMS内存整理功能。

  • -XX:CMSFullGCsBeforeCompaction=设定几次Full后在整理内存,但此过程需要暂停应用。


FullGC

  • 除CMS外,当旧生代和持久代发生GC时,其实对新生代也进行了GC,因此通常称又为FullGC。

  • FullGC触发时首先进行MinorGC(当新生代为PSGC时,可通过-XX:-ScavengeBeforeFullGC禁止FullGC时做MinorGC),然后进行旧生代的GC。

  • 在MinorGC前如果预计到需要移动的对象大小超过旧生代的剩余空间,则采用旧生代的GC方式同时回收新生代和旧生代空间。


FullGC的触发条件

  • 一:旧生代空间不足。   

  • 二:Permanent Generation空间满。

  • 三:统计得到的MinorGC晋升到旧生代的平均大小大于旧生代的剩余空间。(统计方法值得借鉴)

  • 四:CMS时出现promotion failed和concurrent mode failure

  • 五:在程序中显示的调用System.gc()


promotion failed和concurrent mode failure

  • Promotion failed是在进行MinorGC时,Survivor放不下,而此时旧生代也放不下造成的。

  • Concurrent mode failure是在执行CMS时,有对象要放入旧生代,而此时旧生代空间不足造成的。


减少CMS造成FullGC的频率

  • 增大Survivor Space空间或旧生代空间

  • 针对可能的JDK bug会导致CMS remark很久后才触发sweeping,可设置-XX:CMSMaxAbortablePrecleanTime=5来避免


System.gc()

  • 在程序中显示地调用FullGC

  • RMI会通过System.gc()来进行分布式的GC管理,默认为一个小时执行一次,可通过 –Dsun.rmi.dgc.client.gcInterval=3600000来设置。(咱们的FullGC)

  • -XX:+DisableExplicitGC来禁止程序及RMI中显示地调用FullGC。



JAVA TOOLS

  • jinfo

  • jps

  • Jstat

  • jmap

  • Jconsole

  • jstack


JSTAT

  • 一个极强的监视VM内存工具。可以用来监视VM内存内的各种堆和非堆的大小及其内存使用

  • jstat -class pid:显示加载class的数量,及所占空间等信息。

  • jstat -compiler pid:显示VM实时编译的数量等信息。

  • jstat -gc pid:可以显示gc的信息,查看gc的次数,及时间。其中后五项,分别是young gc的次数,young gc的时间,full gc的次数,full gc的时间,gc的总时间。

  • jstat -gccapacity:可以显示,VM内存中三代(young,old,perm)对象的使用和占用大小,如:PGCMN显示的是小perm的内存使用量,PGCMX显示的是perm的内存大使用量,PGC是当前新生成的perm内存占用量,PC是但前perm内存占用量。其他的可以根据这个类推, OC是old内纯的占用量。

  • jstat -gcnew pid:new对象的信息。

  • jstat -gcnewcapacity pid:new对象的信息及其占用量。

  • jstat -gcold pid:old对象的信息。

  • jstat -gcoldcapacity pid:old对象的信息及其占用量。

  • jstat -gcpermcapacity pid: perm对象的信息及其占用量。

  • jstat -printcompilation pid:当前VM执行的信息。


JMAP

  • 显示java进程内存使用的相关信

  • jmap pid                 打印内存使用的摘要信息

  • jmap –heap pid    java heap信息

  •  jmap -histo:live    统计对象count ,live表示在使用    

  •  jmap -histopid >mem.txt 

  •        打印比较简单的各个有多少个对象占了多少内存的

  •        信息,一般重定向的文件

  •  jmap -dump:format=b,file= mem.dat pid       

  •        将内存使用的详细情况输出到mem.dat 文件


JSTACK

  • jstack -- 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。

  • jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息

  • jstack [-l] <pid>        连接正在运行的进程

  • jstack -F [-m] [-l] <pid> 连接挂起的进程

  • jstack [-m] [-l] <executable> <core> 连接core文件

  • jstack [-m] [-l] [server_id@]<remote server IP or hostname> 连接远程服务器


图形化的Jconsole

  • 是一个用java写的GUI程序,用来监控VM,并可监控远程的VM

  • 服务器端配置,远程调控是如下配置:在服务端启动时添加如下参数:

  • -Dcom.sun.management.jmxremote.port=22801

  •  -Dcom.sun.management.jmxremote.pwd.file=jmxremote.pwd 

  • -Dcom.sun.management.jmxremote.ssl=false 

  • -Dcom.sun.management.jmxremote.authenticate=false

  • 在相同目录下创建文件jmxremote.pwd,并填写用户名和密码


相关文章