JVM的内存模型与垃圾回收(整理)

一、JVM的内存模型:

从大的方面来讲,JVM的内存模型分为两大块:

永久区内存( Permanent space )和堆内存(heap space)。

栈内存(stack space)一般都不归在JVM内存模型中,因为栈内存属于线程级别。

每个线程都有个独立的栈内存空间。

Permanent space里存放加载的Class类级对象如class本身,method,field等等。

heap space主要存放对象实例和数组。

heap space由Old Generation和New Generation组成,Old Generation存放生命周期长久的实例对象,而新的对象实例一般放在New Generation。

New Generation还可以再分为Eden区(圣经中的伊甸园)、和Survivor区,新的对象实例总是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,可以向Old区转移活动的对象实例。

 

下图是JVM在内存空间(堆空间)中申请新对象过程的活动图: 

没错,我们常见的OOM(out of memory)内存溢出异常,就是堆内存空间不足以存放新对象实例时导致。

永久区内存溢出相对少见,一般是由于需要加载海量的Class数据,超过了非堆内存的容量导致。通常出现在Web应用刚刚启动时,因此Web应用推荐使用预加载机制,方便在部署时就发现并解决该问题。 

栈内存也会溢出,但是更加少见。

堆和栈:

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

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

-->堆和栈分离的好处:面向对象的设计,当然除了面向对象的设计带来的维护性,复用性和扩展性方面的好处外,我们看看面向对象如何巧妙的利用了堆栈分离。如果从JAVA内存模型的角度去理解面向对象的设计,我们就会发现对象它完美的表示了堆和栈,对象的数据放在堆中,而我们编写的那些方法一般都是运行在栈中,因此面向对象的设计是一种非常完美的设计方式,它完美的统一了数据存储和运行。

堆内存优化:

调整JVM启动参数-Xms  -Xmx   -XX:newSize -XX:MaxNewSize,如调整初始堆内存和最大对内存 -Xms256M -Xmx512M。 或者调整初始New Generation的初始内存和最大内存 -XX:newSize=128M -XX:MaxNewSize=128M。 

永久区内存优化:

调整PermSize参数   如  -XX:PermSize=256M -XX:MaxPermSize=512M。

栈内存优化:

调整每个线程的栈内存容量  如  -Xss2048K 

最终,一个运行中的JVM所占的内存= 堆内存  +  永久区内存  +  所有线程所占的栈内存总和 。

 

二、垃圾回收

以下内容转自http://blog.csdn.net/dc_726/article/details/7934101

垃圾回收包含的内容不少,但顺着下面的顺序捋清知识也并不难。首先要搞清垃圾回收的范围(栈需要GC去回收吗?),然后就是回收的前提条件;
如何判断一个对象已经可以被回收(这里只重点学习根搜索算法就行了),之后便是建立在根搜索基础上的三种回收策略,最后便是JVM中对这三种策略的具体实现。
 
1.范围:要回收哪些区域?
 
Java方法栈、本地方法栈以及PC计数器随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。Java堆和方法区是GC回收的重点区域,因为一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一样,而这两个区域又对立于栈可能随时都会有对象不再被引用,因此这部分内存的分配和回收都是动态的。
 
2.前提:如何判断对象已死?
 
    (1)引用计数法
 
引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,但是解决不了循环引用的问题。比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。
 
下面是循环引用的例子,在Hotspot JVM下可以被正常回收,可以证实JVM采用的不是简单的引用计数法。通过-XX:+PrintGCDetails输出GC日志。
  1. package com.cdai.jvm.gc;  
  2.   
  3. public class ReferenceCount {  
  4.   
  5.     final static int MB = 1024 * 1024;  
  6.       
  7.     byte[] size = new byte[2 * MB];  
  8.       
  9.     Object ref;  
  10.       
  11.     public static void main(String[] args) {  
  12.         ReferenceCount objA = new ReferenceCount();  
  13.         ReferenceCount objB = new ReferenceCount();  
  14.         objA.ref = objB;  
  15.         objB.ref = objA;  
  16.           
  17.         objA = null;  
  18.         objB = null;  
  19.           
  20.         System.gc();  
  21.         System.gc();  
  22.     }  
  23.   
  24. }  
[Full GC (System) [Tenured: 2048K->366K(10944K), 0.0046272 secs] 4604K->366K(15872K), [Perm : 154K->154K(12288K)], 0.0046751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
 
    (2)根搜索
 
通过选取一些根对象作为起始点,开始向下搜索,如果一个对象到根对象不可达时,则说明此对象已经没有被引用,是可以被回收的。可以作为根的对象有:栈中变量引用的对象,类静态属性引用的对象,常量引用的对象等。因为每个线程都有一个栈,所以我们需要选取多个根对象。
 
 
************附:对象复活 
在根搜索中得到的不可达对象并不是立即就被标记成可回收的,而是先进行一次标记放入F-Queue等待执行对象的finalize()方法,执行后GC将进行二次标记,复活的对象之后将不会被回收。因此,使对象复活的唯一办法就是重写finalize()方法,并使对象重新被引用。
  1. package com.cdai.jvm.gc;  
  2.   
  3. public class DeadToRebirth {  
  4.   
  5.     private static DeadToRebirth hook;   
  6.       
  7.     @Override  
  8.     public void finalize() throws Throwable {  
  9.         super.finalize();  
  10.         DeadToRebirth.hook = this;  
  11.     }  
  12.       
  13.     public static void main(String[] args) throws Exception {  
  14.         DeadToRebirth.hook = new DeadToRebirth();  
  15.         DeadToRebirth.hook = null;  
  16.         System.gc();  
  17.         Thread.sleep(500);  
  18.         if (DeadToRebirth.hook != null)  
  19.             System.out.println("Rebirth!");  
  20.         else  
  21.             System.out.println("Dead!");  
  22.           
  23.         DeadToRebirth.hook = null;  
  24.         System.gc();  
  25.         Thread.sleep(500);  
  26.         if (DeadToRebirth.hook != null)  
  27.             System.out.println("Rebirth!");  
  28.         else  
  29.             System.out.println("Dead!");  
  30.     }  
  31.       
  32. }  
要注意的两点是:
第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。
第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。
 
 
3.策略:垃圾回收的算法
 
(1)标记-清除
 
没错,这里的标记指的就是之前我们介绍过的两次标记过程。标记完成后就可以标记为垃圾的对象进行回收了。怎么样,简单吧。但是这种策略的缺点很明显,回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。虽然缺点明显,这种策略却是后两种策略的基础。正因为它的缺点,所以促成了后两种策略的产生。
 
 
(2)标记-复制
 
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点,可用内存变为一半了!
 
怎样解决呢?聪明的程序员们总是办法多过问题的。可以将堆不按1:1的比例分离,而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。
 
从下图可以看到这三块区域如何配合完成GC的,具体的对象空间分配以及晋升请参加后面第6条补充。
 
 
为什么不是全部呢?如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。那么老年代也使用标记-复制策略吧?当然不行!老年代中的对象可不像新生代中的,每次回收都会清除掉大部分。如果贸然采用复制的策略,老年代的回收效率可想而知。
 
(3)标记-整理
 
根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将所有存活下来的对象都向一端移动。
 
 
 
4.实现:虚拟机中的收集器
 
(1)新生代上的GC实现
 
Serial:单线程的收集器,只使用一个线程进行收集,并且收集时会暂停其他所有工作线程(Stop the world)。它是Client模式下的默认新生代收集器。
 
ParNew:Serial收集器的多线程版本。在单CPU甚至两个CPU的环境下,由于线程交互的开销,无法保证性能超越Serial收集器。
 
Parallel Scavenge:也是多线程收集器,与ParNew的区别是,它是吞吐量优先收集器。吞吐量=运行用户代码时间/(运行用户代码+垃圾收集时间)。
另一点区别是配置-XX:+UseAdaptiveSizePolicy后,虚拟机会自动调整Eden/Survivor等参数来提供用户所需的吞吐量。我们需要配置的就是内存大小-Xmx和吞吐量GCTimeRatio。
 
(2)老年代上的GC实现
 
Serial Old:Serial收集器的老年代版本。
 
Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代采用PS GC的话,老年代只有Serial Old能与之配合。现在有了Parallel Old与之配合,可以在注重吞吐量及CPU资源敏感的场合使用了。
 
CMS:采用的是标记-清除而非标记-整理,是一款并发低停顿的收集器。但是由于采用标记-清除,内存碎片问题不可避免。可以使用-XX:CMSFullGCsBeforeCompaction设置执行几次CMS回收后,跟着来一次内存碎片整理。
 
 
5.触发:何时开始GC?
 
Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代。而Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:
 
(1)老年代空间不足
 
(2)PermSpace空间不足
 
(3)统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
 
这里注意一点:PermSpace并不等同于方法区,只不过是Hotspot JVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区。
 
 
6.补充:对象的空间分配和晋升
 
(1)对象优先在Eden上分配
 
(2)大对象直接进入老年代
 
虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到老年代中。因为新生代采用的是标记-复制策略,在Eden中分配大对象将会导致Eden区和两个Survivor区之间大量的内存拷贝。
 
(3)长期存活的对象将进入老年代
 
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中。
 
 
转载另一篇:

引用博文:

The Java Memory Architecture http://blog.codecentric.de/en/2010/01/the-java-memory-architecture-1-act/

JVM内存管理总结 http://blog.csdn.net/lengyuhong/article/details/5953544

JVM内存管理-深入垃圾收集器与内存分配策略 http://www.iteye.com/topic/802638

JVM内存管理-深入Java内存区域与OOM http://www.iteye.com/topic/802573

图解JVM内存模型 http://longdick.iteye.com/blog/473866 

图解JVM在内存中申请对象及垃圾回收流程 http://longdick.iteye.com/blog/468368 

一次Java垃圾收集调优实战 http://www.iteye.com/topic/212967

JVM参数表 http://blogs.oracle.com/watt/resource/jvm-options-list.html



JVM的内部结构如下图:



 

JVM主要包括两个子系统和两个组件:

 

 

1. 两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统;

 

1.1 Class loader子系统的作用:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Class loader。

 

1.2 Execution engine子系统的作用:执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。

 

2. 两个组件分别是Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。

 

2.1 Native interface组件:与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。

 

2.2 Runtime Data Area组件:这就是我们常说的JVM的内存了。它主要分为五个部分——

1、Heap (堆):一个Java虚拟实例中只存在一个堆空间,Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存。

Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

 

2、Method Area(方法区域):被装载的class的信息存储在Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。叫“方法区”可能认识它的人还不太多,如果叫永久代(Permanent Generation)它的粉丝也许就多了。它还有个别名叫 做Non-Heap(非堆)。

方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。

运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。

 

3、Java Stack(java的栈):虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈。栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。


4、Program Counter(程序计数器):每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。

  
5、Native method stack(本地方法栈):保存native方法进入区域的地址.
以上五部分只有Heap 和Method Area是被所有线程的共享使用的;而Java stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有自己的部分。

 

此外还有本机直接内存的管理(Direct Memory) -- 直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。

显然本机直接内存的分配不会受到Java堆大小的限制,但是即然是内存那肯定还是要受到本机物理内存(包括SWAP区或者Windows虚拟内存)的限制的,一般服务器管理员配置JVM参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),而导致动态扩展时出现OutOfMemoryError异常。

 

 

 

JVM内存模型实例以及参数对应:

 

 

  • Model1
 
 
 
  • Model2
 
 
  • 对照表:

Model-1

Model-2

Exception

JVM Options

Method Area

Perm

java.lang.OutOfMemoryError: PermGen space

-XX:PermSize=<value>

-XX:MaxPermSize=<value>

Heap

Young Tenured

java.lang.OutOfMemoryError: Java heap space

-Xms<size>

-Xmx<size>

-Xmn<size>

-XX:newSize

-XX:MaxNewSize

-XX:NewRatio=<value>

-XX:SurvivorRatio=<value>

Thread-1…N

NULL

java.lang.StackOverflowError

-Xss<size>

*Memory Size of Runtime JVM = Heap + Perm + Sum(Thread-1...N)

posted @ 2013-07-18 11:23  kivi  阅读(5752)  评论(0编辑  收藏  举报