jvm-GC详解

jdk内存实际是jvm内存,jvm有一个运行时数据区,其实就是对这一部分的大小分配。运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)

 

Xss:每个线程的stack大小(栈)

Xmx:JAVA HEAP的最大值、默认为物理内存的1/4

Xms:JAVA HEAP的初始值,server端最好Xms与Xmx一样

Xmn:JAVA HEAP young区的大小

XX:PermSize:设定内存的永久保存区域

XX:MaxPermSize:设定最大内存的永久保存区域

 

新生代、老年代、永久代

1、Eden Space (heap)  内存最初从这个线程池分配给大部分对象。

2、Survivor Space (heap) 用于保存在eden space内存池中经过垃圾回收后没有被回收的对象。

3、Tenured Generation (heap) 用于保持已经在survivor space内存池中存在了一段时间的对象。

4、Permanent Generation (non-heap) 保存虚拟机自己的静态(reflective)数据,例如类(class)和方法(method)对象。Java虚拟机共享这些类数据。这个区域被分割为只读的和只写的。

5、Code Cache (non-heap) HotSpot Java虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)。

 

 

 

什么叫永久区?

方法区又被称为堆的逻辑部分(JDK1.8移除了它),它主要存储静态变量以及已加载的类信息还有一些编译后的代码等等。堆空间是我们比较关心的部分,也是GC工作的主要部分,它主要存放了实例对象以及数组对象和常量池。

主要是JVM在运行过程中,存放Class的静态信息,Main方法信息,常量信息,静态方法和变量信息,共享变量等信息。一般很少被JVM进行回收。一般的动态替换Class的行为都是在这个区域来进行的。

Jdk1.8改变版本原因:

在jdk1.8之前的版本,我们通常把堆分为新生代,老年代和永久代(方法区)(我们通常认为方法区也是属于堆的),新生代又包含了eden,from和to(survivor),当对象存活了超过两个survivor的时候(在To里面通常由个年龄阈值作为晋升判断)会被转移到老年代中,这时候这个对象的数据就会存储在方法区内(GC在方法区的收益甚微),当这边的内存不够时就会报OOMError:PermGen,这时候就可能会发生内存泄露问题。为了解决这个问题,jdk1.8完全把存放元数据的永久内存从堆内存转到了本地内存。

<=jdk1.7

 

Jdk1.8

 

在JDK1.8中,取消了PermGen永久区,取而代之的是Metaspace元空间,所以PermSize和MaxPermSize参数失效,取而代之的是

 

 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m

从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间。Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。

 

在移除了Perm区域之后,JDK 8中使用MetaSpace来替代,这些空间都直接在堆上来进行分配。  在JDK8中,类的元数据存放在native堆中,这个空间被叫做:元数据区。JDK8中给元数据区添加了一些新的参数。

 

 默认情况下,类元数据的分配仅受限于可用的本地内存。我们可以使用新的MaxMetaspaceSize参数限定类元数据可用的本地内存的数量。它类似于MaxPermSize。当类元数据区使用量到达MetaspaceSize(32位机客户端模式12M,32位服务器模式16M,64位机会更大)的时候,会触发垃圾回收,然后回收掉无用的类加载器和class对象。      

     MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。

 

1.GC执行的时间点:具体来讲,这个时间并不是由我们控制,而是由JVM里面设置的  GC策略决定的,即使我们调用System.gc()也没法保证即时生效。

 

2.GC执行针对的目标:简单来说,GC针对的目标时被GC收集器判定为死亡的对象,通常我 们使用可达性分析算法来判定。以一系列GC Roots为起点,如果有 某个对象没有任何一个GC Roots对其可达(无引用关联)的时候, 就会判定这个对象已是可回收的对象,但是这时候对象并没有被完全 回收。

 

 3.GC执行的流程:接着2来说,当GC拿到一个‘死亡’对象的时候会给其添加一个标记, 然后进行筛选,筛选的条件是该对象是否有必要执行finalize()方法, 如果该方法已经被虚拟机执行过finalize方法或者没有覆盖finlize方法, 则立即回收,否则将它置于一个F-Queue队列里面并由一个低优先级的 Finalizer线程执行它。

 

4.GC使用的算法: ①标记-清除算法对待回收的对象进行标记。

算法缺点:效率问题,标记和清除过程效率都很低;空间问题,收集之后会   产生大量的内存碎片,不利于大对象的分配。

复制算法将可用内存划分成大小相等的两块A和B,每次只使用其中 一块,当A的内存用完了,就把存活的对象复制到B,并清空A的 内存,不仅提高了标记的效率,因为只需要标记存活的对象,同时 也避免了内存碎片的问题,代价是可用内存缩小为原来的一半。

标记-整理算法在老年代中,对象存活率较高,复制算法的效率很低。在标 记-整理算法中,标记出所有存活的对象,并移动到一端, 然后直接清理边界以外的内存。对象标记过程在可达性分析过程中, 为了准确找出与GC Roots相关联的对象,必须要求整个执行引擎 看起来像是被冻结在某个时间点上,即暂停所有运行中的线程,不 可以出现对象的引用关系还在不断变化的情况。

 

5.内存回收的具体实现(针对HotSpot VM):

        ①Serial 收集器 :单线程(Stop The World) 使用标记整理算法,新生代收集器

        ②ParNew收集器:Serial的多线程版本,新生代收集器

        ③Parallel Scavenge收集器:以吞吐量为目标的收集器。Throughtout=运行用户代码     时间/(垃圾收集时间+运行用户代码时间)

        ④Serial Old 收集器:Serial 的老年代版本

        ⑤Parallel Old收集器:Parallel Scavenge收集器的老年代版本

        ⑥CMS收集器:以获取最短回收停顿时间为目标,使用标记清除算法,整个过程是跟  随余户线程并发执行的,包含4个步骤:

            1)初始标记 2)并发标记 3)重新标记 4)并发清除   其中重新标记是为了修正在用户程序运作而导致变动的那部分对象的标记记录。。它的缺点是对CPU资源敏感和会产生大量内存碎片,还有就是在标记过后用户线程产生的垃圾没法及时清理,必须等待下一次GC

        ⑦G1 收集器:目标是取代CMS收集器并且能独立进行对整个堆空间GC。

        优点:1)能使用多cpu缩短停顿,通过并发使得用户线程不需要停顿 2)空间整合:整体基于标记整理算法,局部使用复制算法--不产生内存碎片 3)停顿可预测,并且能由用户指定 4)分代收集 第三点的实现实际上是把全区域分割成多个Region进行回收并跟踪,并将各个Region垃圾堆积的价值大小建立优先级并按照优先级回收

 

 

如何判断对象是否存活:

GC动作发生之前,需要确定堆内存中哪些对象是存活的,一般有两种方法:引用计数法和可达性分析法。

1、引用计数法

        在对象上添加一个引用计数器,每当有一个对象引用它时,计数器加1,当使用完该对象时,计数器减1,计数器值为0的对象表示不可能再被使用。

引用计数法实现简单,判定高效,但不能解决对象之间相互引用的问题。

2、可达性分析法

        通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链”,以下对象可作为GC Roots:

        本地变量表中引用的对象

        方法区中静态变量引用的对象

        方法区中常量引用的对象

        Native方法引用的对象

当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。

在可达性分析法中,判定一个对象objA是否可回收,至少要经历两次标记过程:

        1、如果对象objA到 GC Roots没有引用链,则进行第一次标记。

        2、如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法。finalize()方法是对象逃脱死亡的最后机会,GC会对队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立联系,那么在第二次标记时,objA会被移出“即将回收”集合。

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。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调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

  1.  年老代(Tenured)被写满
  2.  持久代(Perm)被写满 
  3.  System.gc()被显示调用 
  4. 上一次GC之后Heap的各域分配策略动态变化

 

  1. Permanet Generation空间满
    PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
    java.lang.OutOfMemoryError: PermGen space
    为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
    2、CMS GC时出现promotion failed(晋升)和concurrent mode failure(并发模式)
    对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
    Promotion failed是在进行Minor(二流) GC时,survivor space(占用空间)放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。

JVM调优建议:

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空间的比例

 

 

猜你喜欢

转载自blog.csdn.net/D_J1224/article/details/107530099