深入理解jvm-垃圾回收

本文参考圣思园张龙深入理解jvm

目录

Jvm垃圾回收(GC)模型

垃圾判断算法

引用计数算法(Reference Counting)

根搜索算法(Root Tracing)

方法区:

Jvm常见的GC算法

标记-清除算法(Mark-Sweep):

复制算法(Copying):

标记-整理算法(Mark-Compact):

分代算法(generational)*:

GC补充介绍

垃圾回收器的实现和选择

Serial收集器:

ParNew收集器;

Parallel Scavenge收集器:

Serial old收集器:

Parallel Old收集器:

java内存泄露原因分析

对象定义在错误的范围(wrong Scope):

异常(Expection)处理不当:

集合数据管理不当:

实例

安全点和安全区

枚举根节点:

安全点:

安全区域:


 JVM运行时数据区域-例子

上述方法生产了2部分的内存区域。1)obj这个引用变量,因为是方法内的变量,放到JVM Stack里面。2)真正Object class实例对象,放到Heap里

上述的new语句共消耗了12个bytes,jvm规定引用占4个bytes(在jvm stack),而空对象是8个bytes(在heap)

方法结束后,对应的Stack中的变量马上回收,但是heap中的对象要等到gc来回收。

 

Jvm垃圾回收(GC)模型

  1. 垃圾判断算法
  2. GC算法
  3. 垃圾回收器的实现和选择

垃圾判断算法

  1. 引用计数算法(Reference Counting)
  2. 根搜索算法(Root Tracing)

引用计数算法(Reference Counting

给对象添加一个引用计数器,当有一个地方引用它,计数器+1,当引用失效,计数器-1,任何时刻计数器为0的对象就是不可能再被使用的

引用计数算法无法解决对象循环引用的问题

对象AB之间互相引用,然后外部有其他的值分别引用AB,当外部的引用都失效时,实际上AB都没用了,但是他们的计数都还是1,安装次算法无法被回收

根搜索算法(Root Tracing

在实际的生产语言中(java,C#等),都是使用跟搜索算法判定对象是否存活

算法的基本思路就是通过一系列的成为“GC Roots”的点作为起始进行想向下搜索,当一个对象到GC roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。

在java语言中,GC roots 包括:

  1. 在vm栈(帧中的本地变量)中的引用
  2. 方法区中的静态引用
  3. JNI(即一般说的native方法)中的引用

 

方法区:

Jvm规范表示可以不要求vm在这个区实现gc,这区gc的“性价比”一般比较低。在堆中,尤其是在新生代,常规应用进行一次gc一般可以回收70%-95%的空间,而方法区的gc效率远小于次。当前商业的jvm都有事先方法区的gc,主要回收两部分内容:废弃常量与无用类。

类的回收需要满足如下3个条件:1.该类所有的实例都已经被GC,也就是jvm中不存在该Class的任何实例。2.加载该类的ClassLoader已经被GC。3.该类对应的java.lang.Class对象没有再任何地方被引用,如不能在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理,CGlib等字节码框架、动态生成JSp以及OSGi这类频繁自定义的ClassLoader的场景都需要JVM具备类卸载的支持以保证方法区不会溢出。

Jvm常见的GC算法

  1. 标记-清除算法(Mark-Sweep)
  2. 标记-整理算法(Mark-Compact)
  3. 复制算法(Copying)
  4. 分代算法(generational)*

(之所以复制算法会在新生代使用,1.新生代的数据存在消除比较频繁,大量数据回收。2.新生代存在eden from to三个空间,对于存在不下的数据,老年代对于新生代是一种分配担保的空间。老年代如果采用复制算法,势必也要存在一个分配担保的机制。)

标记-清除算法(Mark-Sweep):

算法分为“标记 ”和“清楚”两个阶段。首先标记出所有需要回收的对象,然后回收所有需要回收的对象。

缺点:效率问题,标记清除这两个效率都不高,需要扫描所有的对象,堆越大,GC越慢。空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前出发另一次的垃圾搜集动作,GC越多,碎片越严重。

 

复制算法(Copying):

将可用的内存划分为2块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块的上面,然后就把原来整块内存一次清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配是也就不用考虑内存碎片等复杂情况,只要一动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。但是这种算法的代价是将内存缩小为原来的一半,代价高昂

现在的商业虚拟机中都是用了这一种收集算法来回收新生代。将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性拷贝到另一块survivor空间上,然后清理掉eden和用过的survivor。Oracle Hotspot 虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。

复制算法在对象存活率比较高时,效率有所下降。如不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

只需要扫描存活的对象,效率更高。不会产生碎片。需要浪费额外的内存作为复制区。复制算法非常适合生命周期比较短的对象,因为每次gc总能回收大部分的对象,复制的开销比较小。根据IBM的专门研究,98%的java对象只会存活1个gc周期,对这些对象很适合用复制算法。并且不用1:1的划分工作区和复制区的空间。

 

标记-整理算法(Mark-Compact):

标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一段移动,然后直接清理掉这端边界以外的内存。

没有内存碎片,比mark-sweep耗费更多的时间进行compact。

 

分代算法(generational)*

当前商业虚拟机的gc都是采用分代收集Generational Collecting 算法,根据对象不同的存活周期将内存划分为几块。一般是吧java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批的对象回收,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。

针对不同生命周期采用不同的gc算法。

JVM6中:

新生代:

对象在Eden中生成,当eden满的时候,还存活的对象会复制到一个survivor区,当此survivor满时,此区的存活对象将被复制到另一个survivor区,当第二个survivor也满的时候,从第一个survivor区复制过来的并且此时还存活的对象,将被复制到老年代。2个survivor是完全对称,轮流替换。

老年代:

存放经过一次或者多次gc还存活的对象。有多种gc器可以选择,每种gc器可以看作一个gc算法的具体实现。可以根据具体需求(吞吐量,相应时间)来选择。

JDK 8中使用MetaSpace来替代,图上的参数也改为-XX:MetaspaceSize

GC补充介绍

 

内存分配:

堆上分配:大多数在eden上分配,少数情况会在old里分配,细节取决于gc的实现。

栈上分配:原子类型的局部变量。

 

内存回收:

gc要做的是将那些dead的对所占用的空间回收掉。Hotspot认为没有引用的对象是dead的。并将引用分为:Strong ,soft , weak, phantom。Strong即默认通过Object o = new Object()这种方式赋值的引用。soft , weak, phantom这三种则都是集成reference。

在full gc时会对reference类型的引用进行特殊处理。

Soft:内存不够时一定会被gc,长期不用也会被gc。

Weak:一定会被gc,当标记为dead,会在referenceQueue中通知。

Phantom:本来就没引用,当从jvm heap中释放时会通知。

 

垃圾回收算法:

 

Gc的时机

在分代模型基础上,gc从时机上分为:scavenge GC和full gc。

Scavenge gc(Minor GC):

触发发时机:新对象生成时,eden空间满了。理论上eden区大多数对象会在scavenge gc回收,复制算法执行效率会很够,scavenge gc时间比较短。

Full gc

对整个jvm进行整理,包括yong,old。主要触发的时机,1)old满了,2)system.gc()。效率低,尽量减少full gc。

在gc中,没有一个完美的算法来解决。

 

Gc的并行与并发

并行(parallel):指多个收集器的线程同时工作,但是用户线程处于等待状态。

并发(Concurrent):指收集器在工作的同时可以运行用户线程工作。

并发不代表解决了gc停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候,但是在清楚垃圾时,用户线程可以和gc线程并发执行。

垃圾回收器的实现和选择

Serial收集器

单线程收集器,收集是会暂停所有的工作线程(stop the world STW),使用复制收集算法,vm运行在client模式时的默认新生代收集器。最早的收集器,单线程进行gc。新生代老年代都可以使用。在新生代,采用复制算法;在老年代采用mark-compact算法。因为是单线程gc,没有多线程切换的额外开销,简单实用。Hotspot Client模式缺省的垃圾收集器:

 

ParNew收集器;

ParNew收集器就是serial的多线程版本,除了使用多个收集器线程外,其余行为包括算法、STW、对象分配规则、回收策略等,都与Serial收集器一模一样。对应的这种收集器是vm运行在server模式的默认新生代收集器。在单cpu的环境中,parnew收集器并不会比serial收集器有更好的效果。只有在多cpu下才会施展其效率。可以通过-XX:ParallelGCThreads来控制gc线程数多少,并需要结合具体cpu数量。

 

Parallel Scavenge收集器

此收集器也是一个多线程收集器,使用的复制算法,但是它的对象分配规则和回收策略对于parnew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。

 

Serial old收集器

Serial old是单线程收集器,使用标记-整理算法,是老年代的收集器。

 

Parallel Old收集器:

老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,jvm1.6提供,在此之前,新生代使用了ps收集器的话,老年代除serial old外别无选择,因为ps无法与cms收集器配合工作。Parallel scavenge在老年代的实现,并且是在jvm1.6出现的。采用多线程,mark-Compact算法。Parallel scavenge + Parallel Old = 高吞吐量,但gc停顿可能不理想。

 

CMS(Concurrent Mark Sweep)收集器

CMS是一种以最短停顿时间为目标的收集器,使用cms并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务停顿时间,CMS使用标记清除算法。非常适合web应用。只针对老年代,一般结合ParNew。Concurrent,GC线程和用户线程并发工作(尽量并发)。标记清除算法。只有在多CPU下才有意义。使用-XX:+UseConcMarkSweepGC打开

 

 

java内存泄露原因分析

 

Java内存泄露的经典原因:

对象定义在错误的范围(wrong Scope)

异常(Expection)处理不当

集合数据管理不当

 

对象定义在错误的范围(wrong Scope

如果Foo实例对象的生命较长,会导致临时性的内存泄露。Jvm喜欢生命周期短的对象。

 

 

异常(Expection)处理不当:

 

集合数据管理不当:

当使用Array-based的数据结构(ArrayList,HashMap等)尽量减少resize。比如new ArrayList是,尽量估算size,在创建的时候吧size确定。减少resize可以避免没有必要的array copying,gc碎片等问题。

如歌一个list只需要顺序访问,不需要随机访问(random access),用linkedlist代替ArrayList。Linkedlist本质是链表,不需要resize,但只适用于顺序访问。

实例

EG1.查看垃回收过程

-verbose:gc 会输出详细的垃圾回收信息

-Xms20M   初始值20m 堆空间

-Xmx20M   最大值20m

-Xmn10M     堆空间中,新生代的大小是10m

-XX:+PrintGCDetails  打印出垃圾回收的详细信息。

-XX:SurvivorRatio=8   eden:survivor 是8:1 但是survivor是两个。

GC(原因)

PSYoungGen【PS是Parallel Scavenge收集器,剩下代表年轻代】

左侧->右侧() 【左侧大小代表回收前的存活的对象占的空间,右侧代表回收后存活的空间,括号表示总的新生代空间】

左侧是gc前总的堆的大小(包括新生和老年),回收后总的堆的大小,括号里是总的堆的容量。

老年代是4104k的来源:

 

在jdk1.8中默认的新生代垃圾回收器是Parallel Scavenge,老年代是Parallel Old

 

控制台中输入java –XX:+PrintCommandLineFlags –version

-XX:+UseCompressedClassPointers 对类指针进行压缩。 -XX:+UseCompressedOops   32的jvm迁移到64的时候,指针会膨胀,会导致更多的内存空间占用。改命令会压缩这些空间。  -XX:-UseLargePagesInd ividualAllocation和oops是一起使用的,在大页内存使用发生时这个选项也会自动启用-XX:+UseParallelGC 决定新生代老年代是采用什么垃圾收集器

 

在程序中,我们增加一个vm选项,PretenureSizeThreshold表示当新创建的对象的大小超过这个阈值话,这个对象就不会再新生代创建,而直接在老年代分配。配合使用+UseSerialGC 图上少一个加号。

 

-XX:MaxTenuringThreshold=5         可以自动调节对象晋升(promote)到老年代阈值的gc中,设置阈值的最大值。默认值为15,CMS默认值为6,G1默认为15(在jvm中,该值是由4个bit来表示的,所以最大值是1111,即15)

-XX:+PrintTenuringDistribution           打印各年龄段对象

 

在经历多次的gc后,存活的对象会在from survivor 与to survivor 之间来回存放,而这里一个前提则是这两个空间有足够的大小来存放这些数据,在gc算法中,会计算每个对象的年龄大小,如果到达某个年龄后发现总大小已经大于了Survivor的50%,那么这时就需要调整阈值,不能再继续等到默认的15次gc后再完成晋升,让存活对对象尽快完成晋升。

 

TargetSurvivorRatuo,当survivor空间中所存活的对象容量,超过这个百分比,就会重新计算对象晋升的阈值。

安全点和安全区

 

枚举根节点:

当执行系统停顿来下后,我们不需要一个不漏地检查完所有执行上下文和全局的引用位置。在HotSpot的实现中,是使用一组称为OOPMap的数据结构来达到这个目的的。可达性分析。

 

安全点:

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那将会需要大量的额外空间,这样GC的空间成本将会变得很高。

实际上,HotSpot也的确没有为每条指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint)程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。 

对于Sefepoint,另一个需要考虑的问题是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。这里有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。

抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。(而目前几乎没有vm采用抢占式中断来暂停线程从而响应GC事件)。

主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

 

安全区域:

使用Safepoint似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定。Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。

在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值