现在的位置: 首页 > 综合 > 正文

JVM内存模型以及垃圾收集策略解析(重要)

2019年09月27日 ⁄ 综合 ⁄ 共 11725字 ⁄ 字号 评论关闭

一 JVM内存模型

1.1 Java

Java栈是与每一个线程关联的,JVM在创建每一个线程的时候,会分配一定的栈空间给线程。它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。栈空间随着线程的终止而释放。

StackOverflowError:如果在线程执行的过程中,栈空间不够用,那么JVM就会抛出此异常,这种情况一般是死递归造成的。

1.2 

Java中堆是由所有的线程共享的一块内存区域,堆用来保存各种JAVA对象,比如数组,线程对象等。

1.2.1 Generation

JVM堆一般又可以分为以下三部分:

Ø Perm

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的大小进行控制。

Ø Total Heap 

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

-Xmx:指定JVM堆得最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存

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

Ø Young Generation

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

堆内存

-XX:SurvivorRatio=32意味着eden和一个survivor的比值是321,这样一个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 跟踪收集器

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

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.2.2.2 Copying Collector(复制收集器)

复制收集器将内存分为两块一样大小空间,某一个时刻,只有一个空间处于活跃的状态,当活跃的空间满的时候,GC就会将活跃的对象复制到未使用的空间中去,原来不活跃的空间就变为了活跃的空间。

复制收集器具体过程可以参考下图:

优点:

只扫描可以到达的对象,不需要扫描所有的对象,从而减少了应用暂停的时间

缺点:

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

2.复制对象需要一定的开销

2.2.2.3 Mark-Compact Collector(标记-整理收集器)

标记整理收集器汲取了标记清除和复制收集器的优点,它分两个阶段执行,在第一个阶段,首先扫描所有活跃的对象,并标记所有活跃的对象,第二个阶段首先清除未标记的对象,然后将活跃的的对象复制到堆得底部。标记整理收集器的过程示意图请参考下图:

 

Mark-compact策略极大的减少了内存碎片,并且不需要像Copy Collector一样需要两倍的空间。

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 SurvivorTo 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 gcParallel gc可以用如下的图来表示:

2.3.3 Concurrent Collector

    
Concurrent Collector通过并行的方式进行垃圾收集,这样就减少了垃圾收集器收集一次的时间,这种GC在实时性要求高于吞吐量的时候比较有用。

此种GC可以用参数-XX:UseConcMarkSweepGC配置,此GC主要用于老生代Perm代的收集。

 

参考资料

1 http://developers.sun.com/mobility/midp/articles/garbage/

2

http://developers.sun.com/mobility/midp/articles/garbagecollection2/

3 http://blogs.sun.com/watt/resource/jvm-options-list.html

4

http://java.sun.com/developer/technicalArticles/Programming/turbo/

5

http://www.ibm.com/developerworks/library/j-jtp10283/index.html?S_TACT=105AGX52&S_CMP=cn-a-j

6

http://www.ibm.com/developerworks/library/j-jtp11253/index.html?S_TACT=105AGX52&S_CMP=cn-a-j

7 http://java.sun.com/docs/hotspot/gc1.4.2/

 

 

三 垃圾收集策略配置

3.1 吞吐量优先

吞吐量是指GC的时间与运行总时间的比值,比如系统运行了100分钟,而GC占用了一分钟,那么吞吐量就是99%,吞吐量优先一般运用于对响应性要求不高的场合,比如web应用,因为网络传输本来就有延迟的问题,GC造成的短暂的暂停使得用户以为是网络阻塞所致。

吞吐量优先可以通过-XX:GCTimeRatio来指定。

当通过-XX:GCTimeRatio不能满足系统的要求以后,我们可以更加细致的来对JVM进行调优。

首先因为要求高吞吐量,这样就需要一个较大的Young generation,此时就需要引入“Parallel scavenging Collector”,可以通过参数:-XX:UseParallelGC来配置。

 java -server -Xms3072m -Xmx3072m -XX:NewSize=2560m -XX:MaxNewSize=2560 XX:SurvivorRatio=2 -XX:+UseParallelGC 

当年轻代使用了"Parallel scavenge collector"后,老生代就不能使用"CMS"GC了,在JDK1.6之前,此时老生代只能采用串行收集,而JDK1.6引入了并行版本的老生代收集器,可以用参数-XX:UseParallelOldGC来配置

3.1.1 控制并行的线程数

缺省情况下,Parallel scavenging Collector 会开启与cpu数量相同的线程进行并行的收集,但是也可以调节并行的线程数。假如你想用4个并行的线程去收集Young generation的话,那么就可以配置-XX:ParallelGCThreads=4,此时JVM的配置参数如下:

 java -server -Xms3072m -Xmx3072m -XX:NewSize=2560m -XX:MaxNewSize=2560 XX:SurvivorRatio=2 -XX:+UseParallelGC -XX:ParallelGCThreads=4

3.1.2 自动调节新生代

在采用了"Parallel scavenge collector"后,此GC会根据运行时的情况自动调节survivor ratio来使得性能最优,因此"Parallel scavenge collector"应该总是开启此参数。

此时JVM的参数配置如下:

java -server -Xms3072m -Xmx3072m -XX:+UseParallelGC    -XX:ParallelGCThreads=4 -XX:+UseAdaptiveSizePolicy

 

3.2 响应时间优先

响应时间优先是指GC每次运行的时间不能太久,这种情况一般使用与对及时性要求很高的系统,比如股票系统等。

响应时间优先可以通过参数-XX:MaxGCPauseMillis来配置,配置以后JVM将会自动调节年轻代,老生代的内存分配来满足参数设置。

在一般情况下,JVM的默认配置就可以满足要求,只有默认配置不能满足系统的要求时候,才会根据具体的情况来对JVM进行性能调优。如果采用默认的配置不能满足系统的要求,那么此时就可以自己动手来调节。

此时"Young generation"可以采用"Parallel copying collector",而"Old generation"则可以采用"Concurrent Collector",

举个例子来说,以下参数设置了新生代用Parallel Copying Collector老生代采用CMS收集器

java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m-XX:SurvivorRatio=2       -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 

此时需要注意两个问题:

如果没有指定-XX:+UseParNewGC,则采用默认的非并行版本的copy collector.

2  如果在一个单CPU的系统上设置了-XX:+UseParNewGC ,则默认还是采用缺省的copy collector.

3.2.1 控制并行的线程数

默认情况下,Parallel copy collector启动和CPU数量一样的线程,也可以通过参数-XX:ParallelGCThreads来指定,比如你想用3个线程去进行并发的复制收集,那么可以改变上述参数如下:

java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m-XX:SurvivorRatio=2      -XX:ParallelGCThreads=4  -XX:+UseConcMarkSweepGC     -XX:+UseParNewGC 

3.2.2 控制并发收集的临界值

默认情况下,CMS gc"old generation"空间占用率高于68%的时候,就会进行垃圾收集,而如果想控制收集的临界值,可以通过参数:-XX:CMSInitiatingOccupancyFraction来控制,比如改变上述的JVM配置如下:

java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m-XX:SurvivorRatio=2      -XX:ParallelGCThreads=4  -XX:+UseConcMarkSweepGC     -XX:+UseParNewGC     -XX:CMSInitiatingOccupancyFraction=35

 

四 GC触发以及常见的内存错误

4.1 Minor GC的触发

Minor GC主要负责收集Young GenerationMinor GC一般在新生代不够用的情况下触发,比如我们一次性创建了很多对象等。

List<byte[]> buffer = new ArrayList<byte[]>();for(int i=0;i<8*1024;i++){

buffer.add(new byte[1024]);

}

以上代码通过一个字节数组的List模拟触发Minor gc,设置JVM参数如下:

-verbose:gc -Xmn10M -Xms64M -Xmx64M -XX:+PrintGC

设置以上参数以后,因为-Xmn=10M,默认-XX:SurvivorRatio=8 ,则eden的空间大小为8M,当eden对象大小超过8M的时候就会触发Minor gc.

运行的结果如下:

[GC 8192K->8030K(64512K), 0.0243391 secs]

从运行结果可以看出,gc前和gc后的eden区的占用情况,需要注意的是括号里(64512)这个数值时63M,它不包括一块Survivor 空间。

这里需要注意的一点就是,如果创建的对象大于eden的大小,那么将不会通过Survivor空间复制,直接转移到old generation.

调整以上代码如下:

List<byte[]> buffer = new ArrayList<byte[]>();

buffer.add(new byte[8*1024*1024]);

通过同样的JVM参数运行,则发现不会触发Minor gc,这是因为对象超过了eden的大小,从而直接分配到了Old generation.

 

4.2 Major GC的触发

4.2.1 Old Generation空间满或者接近某一个比例

Old generation 空间满是因为Young generation提升到Old generation的对象+Old generation的本来的大小已经接近或者超过了Old generation的大小。对于CMS GC,当Old generation空间使用率接近某一个比例,可以通过参数-XX:CMS InitialingOccupancyFraction,此参数表示Old generation的使用率,默认为68%

Young generation对象提升到Old generation对象有以下三种情况:

Ø 分配的对象大于eden空间的大小

Ø Young generation代中经过了-XX:MaxTenuringThreshold次复制任然存活的对象

Ø Minor gc的时候,放不进to survivor的对象

Major GC以后,如果还没有足够的空间可以用的话,此时就会抛出java.lang.OutOfMemoryjava heap space,当出现此错误的时候,说明可能存在内存泄露现象的,这时候就需要我们对程序进行检查看看什么地方存在内存泄露的。

我们可以通过以下代码来模拟一下java.lang.OutOfMemory:java heap space的发生:

List<byte[]> buffer = new ArrayList<byte[]>();

buffer.add(new byte[10*1024*1024]);

以上代码分配了一个10M的字节数组,我们通过以下的参数运行:

-verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC

以上参数指定Young generation的空间大小为10MOld generation空间大小为10M

运行结果如下:

[GC 327K->134K(19456K), 0.0056516 secs]

[Full GC 134K->134K(19456K), 0.0178891 secs]

[Full GC 134K->131K(19456K), 0.0141412 secs]

Exception in thread "main" java.lang.OutOfMemoryError:  Java heap space

        at Test.main(Test.java:30)

从运行结果可以看出,JVM进行了一次Minor gc和两次的Major gc,从Major gc的输出可以看出,gc以后old区使用率为134K,而字节数组为10M,加起来大于了old generation的空间,所以抛出了异常,如果调整-Xms21M,-Xmx21M,那么就不会触发gc操作也不会出现异常了。

4.2.2 Perm Generation 空间满

Perm Generation空间主要存放Class对象,Field,Method对象,当一次性加载太多的类或者在热部署以后不卸载类的情况(比如在Jboss服务器中,如果经常热部署一些应用就会出现Perm 空间溢出)就会造成Perm Generation被占满,此时就会出现:

java.lang.OutOfMemory:PermGen space,在出现此异常的时候,如果是因为热部署引起的,我们重新启动AS就可以了,如果是因为加载的类太多,此时可以通过-XX:PermSize-XX:MaxPermSize调整。

4.3 常见内存错误分析

4.3.1 StackOverflowError 

java.lang.StackOverflowError错误表示JVM栈溢出,出现这个错误的原因一般都是递归的层次太深,或者无限的递归造成的。出现这种错误的时候首先要对应用程序进行检查,看看是那些代码造成了栈溢出,如果是递归造成的可以改为迭代方式实现。

JVM同样也提供了一个参数来让我们调节运行时栈空间的大小。-XX:Xss=256K表示栈空间最大为256K.我们也可以调大,但是建议不要对此参数进行调节。

4.3.2 OutOfMemoryError: Java heap space.

java.lang.OutOfMemoryError: Java heap space这个错误表示JVM的新生代和老生代的内存不足。出现这个错误说明应用程序出现了内存溢出或者程序所需要的内存大于JVM的内存设置了。

遇到这个问题的时候,首先我们可以调节JVMHeap内存的大小,具体可以通过-Xmx -Xms来进行设置,如果设置大以后还是会出现内存溢出,那么说明应用程序本身存在内存泄露,这个时候就需要我们对应用程序进行检查,找出导致内存泄露的地方,然后修正。

4.3.3 OutOfMemory:PermGen space

java.lang.OutOfMemory:PermGen space错误是由Perm space空间不足。一般出现这个错误是由加载了太多的类或者大量使用了动态代理造成的。如果出现了这个错误,我们可以将Perm空间调大一点。

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

 

参考资料

1 http://developers.sun.com/mobility/midp/articles/garbage/

2

http://developers.sun.com/mobility/midp/articles/garbagecollection2/

3 http://blogs.sun.com/watt/resource/jvm-options-list.html

4

http://java.sun.com/developer/technicalArticles/Programming/turbo/

5

http://www.ibm.com/developerworks/library/j-jtp10283/index.html?S_TACT=105AGX52&S_CMP=cn-a-j

6

http://www.ibm.com/developerworks/library/j-jtp11253/index.html?S_TACT=105AGX52&S_CMP=cn-a-j

7 http://java.sun.com/docs/hotspot/gc1.4.2/

 

抱歉!评论已关闭.