JVM内存模型及垃圾收集策略解析

AD:java

JVM内存模型是Java的核心技术之一,以前51CTO曾为你们介绍过JVM分代垃圾回收策略的基础概念,如今不少编程语言都引入了相似Java JVM的内存模型和垃圾收集器的机制,下面咱们将主要针对Java中的JVM内存模型及垃圾收集的具体策略进行综合的分析。算法

一 JVM内存模型编程

1.1 Java栈数组

Java栈是与每个线程关联的,JVM在建立每个线程的时候,会分配必定的栈空间给线程。它主要用来存储线程执行过程当中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。StackOverflowError:若是在线程执行的过程当中,栈空间不够用,那么JVM就会抛出此异常,这种状况通常是死递归形成的。缓存

1.2 堆服务器

Java中堆是由全部的线程共享的一块内存区域,堆用来保存各类JAVA对象,好比数组,线程对象等。app

1.2.1 Generation编程语言

JVM堆通常又能够分为如下三部分:大数据

JVM堆的三部分

◆ Permspa

Perm代主要保存class,method,filed对象,这部门的空间通常不会溢出,除非一次性加载了不少的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,形成这个错误的很大缘由就有多是每次都从新部署,可是从新部署后,类的class没有被卸载掉,这样就形成了大量的class对象保存在了perm中,这种状况下,通常从新启动应用服务器能够解决问题。

◆ Tenured

Tenured区主要保存生命周期长的对象,通常是一些老的对象,当一些对象在Young复制转移必定的次数之后,对象就会被转移到Tenured区,通常若是系统中用了application级别的缓存,缓存中的对象每每会被转移到这一区间。

◆ Young

Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另一个留作垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在通过几回垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。

1.2.2 Sizing the Generations

JVM提供了相应的参数来对内存大小进行配置。正如上面描述,JVM中堆被分为了3个大的区间,同时JVM也提供了一些选项对Young,Tenured的大小进行控制。

JVM的相关参数

◆ Total Heap

-Xms :指定了JVM初始启动之后初始化内存

-Xmx:指定JVM堆得最大内存,在JVM启动之后,会分配-Xmx参数指定大小的内存给JVM,可是不必定所有使用,JVM会根据-Xms参数来调节真正用于JVM的内存

-Xmx -Xms之差就是三个Virtual空间的大小

◆ Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,这样eden+2*survivor=1/9

堆内存

-XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.

-Xmn 参数设置了年轻代的大小

◆ Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

1.3 堆栈分离的好处

呵呵,其它的先不说了,就来讲说面向对象的设计吧,固然除了面向对象的设计带来的维护性,复用性和扩展性方面的好处外,咱们看看面向对象如何巧妙的利用了堆栈分离。若是从JAVA内存模型的角度去理解面向对象的设计,咱们就会发现对象它完美的表示了堆和栈,对象的数据放在堆中,而咱们编写的那些方法通常都是运行在栈中,所以面向对象的设计是一种很是完美的设计方式,它完美的统一了数据存储和运行。

二 JAVA垃圾收集器

2.1 垃圾收集简史

垃圾收集提供了内存管理的机制,使得应用程序不须要在关注内存如何释放,内存用完后,垃圾收集会进行收集,这样就减轻了由于人为的管理内存而形成的错误,好比在C++语言里,出现内存泄露时很常见的。Java语言是目前使用最多的依赖于垃圾收集器的语言,可是垃圾收集器策略从20世纪60年代就已经流行起来了,好比Smalltalk,Eiffel等编程语言也集成了垃圾收集器的机制。

2.2 常见的垃圾收集策略

常见的垃圾收集策略

全部的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面得不可到达主要是指应用程序已经没有内存块的引用了,而在JAVA中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在全部线程栈的对象的引用)引用或者对象被另外一个可到达的对象引用。

2.2.1 Reference Counting(引用计数)
 
引用计数是最简单直接的一种方式,这种方式在每个对象中增长一个引用的计数,这个计数表明当前程序有多少个引用引用了此对象,若是此对象的引用计数变为0,那么此对象就能够做为垃圾收集器的目标对象来收集。

优势:

简单,直接,不须要暂停整个应用

缺点:

1.须要编译器的配合,编译器要生成特殊的指令来进行引用计数的操做,好比每次将对象赋值给新的引用,或者者对象的引用超出了做用域等。

2.不能处理循环引用的问题

2.2.2 跟踪收集器

跟踪收集器首先要暂停整个应用程序,而后开始从根对象扫描整个堆,判断扫描的对象是否有对象引用,这里面有三个问题须要搞清楚:

JVM的跟踪收集器

1.若是每次扫描整个堆,那么势必让GC的时间变长,从而影响了应用自己的执行。所以在JVM里面采用了分代收集,在新生代收集的时候minor gc只须要扫描新生代,而不须要扫描老生代。

2.JVM采用了分代收集之后,minor gc只扫描新生代,可是minor gc怎么判断是否有老生代的对象引用了新生代的对象,JVM采用了卡片标记的策略,卡片标记将老生代分红了一块一块的,划分之后的每个块就叫作一个卡片,JVM采用卡表维护了每个块的状态,当JAVA程序运行的时候,若是发现老生代对象引用或者释放了新生代对象的引用,那么就JVM就将卡表的状态设置为脏状态,这样每次minor gc的时候就会只扫描被标记为脏状态的卡片,而不须要扫描整个堆。具体以下图:
3.GC在收集一个对象的时候会判断是否有引用指向对象,在JAVA中的引用主要有四种:Strong reference,Soft reference,Weak reference,Phantom reference.

◆ Strong Reference

强引用是JAVA中默认采用的一种方式,咱们平时建立的引用都属于强引用。若是一个对象没有强引用,那么对象就会被回收。

public void testStrongReference(){  Object referent = new Object();  Object strongReference = referent;  referent = null;  System.gc();  assertNotNull(strongReference);  }

◆ Soft Reference

软引用的对象在GC的时候不会被回收,只有当内存不够用的时候才会真正的回收,所以软引用适合缓存的场合,这样使得缓存中的对象能够尽可能的再内存中待长久一点。

Public void testSoftReference(){  String  str =  "test";  SoftReference<String> softreference = new SoftReference<String>(str);  str=null;  System.gc();  assertNotNull(softreference.get());  }

◆ Weak reference

弱引用有利于对象更快的被回收,假如一个对象没有强引用只有弱引用,那么在GC后,这个对象确定会被回收。

Public void testWeakReference(){  String  str =  "test";  WeakReference<String> weakReference = new WeakReference<String>(str);  str=null;  System.gc();  assertNull(weakReference.get());  }

◆ Phantom reference

2.2.2.1 Mark-Sweep Collector(标记-清除收集器)

标记清除收集器最先由Lisp的发明人于1960年提出,标记清除收集器中止全部的工做,从根扫描每一个活跃的对象,而后标记扫描过的对象,标记完成之后,清除那些没有被标记的对象。

优势:

1 解决循环引用的问题

2 不须要编译器的配合,从而就不执行额外的指令

缺点:

1.每一个活跃的对象都要进行扫描,收集暂停的时间比较长。

2.2.2.2 Copying Collector(复制收集器)复制收集器将内存分为两块同样大小空间,某一个时刻,只有一个空间处于活跃的状态,当活跃的空间满的时候,GC就会将活跃的对象复制到未使用的空间中去,原来不活跃的空间就变为了活跃的空间。复制收集器具体过程能够参考下图:

JVM的复制收集器

优势:

1 只扫描能够到达的对象,不须要扫描全部的对象,从而减小了应用暂停的时间

缺点:

1.须要额外的空间消耗,某一个时刻,老是有一块内存处于未使用状态

2.复制对象须要必定的开销

2.2.2.3 Mark-Compact Collector(标记-整理收集器)标记整理收集器汲取了标记清除和复制收集器的优势,它分两个阶段执行,在第一个阶段,首先扫描全部活跃的对象,并标记全部活跃的对象,第二个阶段首先清除未标记的对象,而后将活跃的的对象复制到堆得底部。标记整理收集器的过程示意图请参考下图:Mark-compact策略极大的减小了内存碎片,而且不须要像Copy Collector同样须要两倍的空间。

JVM标记整理收集器

2.3 JVM的垃圾收集策略
 
GC的执行时要耗费必定的CPU资源和时间的,所以在JDK1.2之后,JVM引入了分代收集的策略,其中对新生代采用"Mark-Compact"策略,而对老生代采用了“Mark-Sweep"的策略。其中新生代的垃圾收集器命名为“minor gc”,老生代的GC命名为"Full Gc 或者Major GC".其中用System.gc()强制执行的是Full Gc.

2.3.1 Serial Collector

Serial Collector是指任什么时候刻都只有一个线程进行垃圾收集,这种策略有一个名字“stop the whole world",它须要中止整个应用的执行。这种类型的收集器适合于单CPU的机器。

Serial Copying Collector

此种GC用-XX:UseSerialGC选项配置,它只用于新生代对象的收集。1.5.0之后。-XX:MaxTenuringThreshold来设置对象复制的次数。当eden空间不够的时候,GC会将eden的活跃对象和一个名叫From survivor空间中尚不够资格放入Old代的对象复制到另一个名字叫To Survivor的空间。而此参数就是用来讲明到底From survivor中的哪些对象不够资格,假如这个参数设置为31,那么也就是说只有对象复制31次之后才算是有资格的对象。这里须要注意几个个问题:

◆  From Survivor和To survivor的角色是不断的变化的,同一时间只有一块空间处于使用状态,这个空间就叫作From Survivor区,当复制一次后角色就发生了变化。

◆  若是复制的过程当中发现To survivor空间已经满了,那么就直接复制到old generation.

◆  比较大的对象也会直接复制到Old generation,在开发中,咱们应该尽可能避免这种状况的发生。

Serial  Mark-Compact Collector

串行的标记-整理收集器是JDK5 update6以前默认的老生代的垃圾收集器,此收集使得内存碎片最少化,可是它须要暂停的时间比较长。

2.3.2 Parallel Collector 

Parallel Collector主要是为了应对多CPU,大数据量的环境。Parallel Collector又能够分为如下两种:

Parallel Copying Collector

此种GC用-XX:UseParNewGC参数配置,它主要用于新生代的收集,此GC能够配合CMS一块儿使用。1.4.1之后Parallel Mark-Compact Collector,此种GC用-XX:UseParallelOldGC参数配置,此GC主要用于老生代对象的收集。1.6.0

Parallel scavenging Collector

此种GC用-XX:UseParallelGC参数配置,它是对新生代对象的垃圾收集器,可是它不能和CMS配合使用,它适合于比较大新生代的状况,此收集器起始于jdk 1.4.0。它比较适合于对吞吐量高于暂停时间的场合,Serial gc和Parallel gc能够用以下的图来表示:

Serial gc和Parallel gc图解

2.3.3 Concurrent Collector

Concurrent Collector经过并行的方式进行垃圾收集,这样就减小了垃圾收集器收集一次的时间,这种GC在实时性要求高于吞吐量的时候比较有用。此种GC能够用参数-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。

并行方式垃圾收集

相关文章
相关标签/搜索