JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途、创建和销毁的时间。本文以Sun公司HotSpot虚拟机为例进行说明,JRockit和IBM J9会有所不同。HotSpot运行时数据区分为堆和栈两种类型,堆空间为线程共享,栈空间为线程私有。堆空间又分为方法区和堆,栈细分为虚拟机栈、本地方法栈和程序计数器,但HotSpot栈的实现将本地方法栈中的内容放入虚拟机栈空间进行存储,所以HotSpot中不存在本地方法栈,下面展开说明图1各区域具体作用:
方法区 (Method Area or Permanent Generation):
线程共享的内存区域,用于存储以被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然这个区域被虚拟机规范把方法区描述为堆的一个逻辑部分,但是它的别名叫NonHeap,用来与堆做区别,通过-XX:permSize和-XX:MaxPermSize设置该空间大小。当方法区无法满足内存分配需求时就会抛OutOfMemoryError。
运行时常量池(Runtime Constant Pool):
它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(ConstantPool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
堆(Heap):
虚拟机管理的最大的一块内存,同时也是被所有线程所共享的,它在虚拟机启动时创建,存在的意义就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。这里存放的对象被自动管理,也就是俗称的GC(GarbageCollector)所管理。Java堆的容量可以通过-Xmx和-Xms参数调整空间大小。堆所使用的内存不需要保证是物理连续的,只要逻辑上是连续的即可。如果堆中没有可用内存完成实例分配并且堆也无法扩展,这时就会抛OutOfMemoryError。
虚拟机栈(Virtual Machine Stack):
每一个线程都有自己的虚拟机栈,这个栈与线程同时创建,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(StackFrame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。JVM Stack可以被实现成固定大小,也可以根据计算动态扩展。如果采用固定大小的JVM Stack设计,那么每一条线程的JVM Stack容量应该在线程创建时独立地选定。JVM实现应该提供调节JVMStack初始容量的手段;如果采用动态扩展和收缩的JVM Stack方式,应该提供调节最大、最小容量的手段。如果线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflowError;如果JVMStack可以动态扩展,但是在尝试扩展时无法申请到足够的内存时抛出OutOfMemoryError,通过-Xss参数可设置栈大小。
本地方法栈(Native Method Stack):
本地方法栈与虚拟机栈作用相似,后者为虚拟机执行Java方法服务,而前者为虚拟机用到的Native方法服务。虚拟机规范对于本地方法栈中方法使用的语言,使用方式和数据结构没有强制规定,甚至有的虚拟机(比如HotSpot)直接把二者合二为一。
程序计数器(Program Counter Register):
它是一块较小的内存空间,它的作用是记录当先线程所执行的字节码的信号指示器。每一条JVM线程都有自己的PC寄存器,各条线程之间互不影响,独立存储,这类内存区域被称为“线程私有”内存在任意时刻,一条JVM线程只会执行一个方法的代码。该方法称为该线程的当前方法(Current Method)如果该方法是java方法,那PC寄存器保存JVM正在执行的字节码指令的地址。如果该方法是native,那PC寄存器的值是undefined。此内存区域是在Java虚拟机规范中唯一没有规定OutOfMemoryError情况出现的区域。
直接内存(Direct Memory):
直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。JDK1.4加的NIO中,ByteBuffer有个方法是allocateDirect(intcapacity) ,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
JVM栈是运行时的单位,而JVM堆是存储的单位。JVM栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;JVM堆解决的是数据存储的问题,即数据怎么放、放在哪儿。在Java中一个线程就会相应有一个线程JVM栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程JVM栈。而JVM堆则是所有线程共享的。JVM栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而JVM堆只负责存储对象信息。
为什么要把JVM堆和JVM栈区分出来呢?JVM栈中不是也可以存储数据吗?
第一,从软件设计的角度看,JVM栈代表了处理逻辑,而JVM堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,JVM堆与JVM栈的分离,使得JVM堆中的内容可以被多个JVM栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,JVM堆中的共享常量和缓存可以被所有JVM栈访问,节省了空间。
第三,JVM栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于JVM栈只能向上增长,因此就会限制住JVM栈存储内容的能力。而JVM堆不同,JVM堆中的对象是可以根据需要动态增长的,因此JVM栈和JVM堆的拆分,使得动态增长成为可能,相应JVM栈中只需记录JVM堆中的一个地址即可。
第四,JVM堆和JVM栈的完美结合就是面向对象的一个实例。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在JVM堆中;而对象的行为(方法),就是运行逻辑,放在JVM栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
JVM栈是程序运行的根本,JVM堆是为JVM栈进行数据存储的服务,简单讲JVM堆就是一块共享的内存。不过,正是因为JVM堆和JVM栈的分离的思想,才使得Java的垃圾回收成为可能。
JVM栈的组成元素——栈帧
栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入JVM栈。
局部变量区:局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引是3、4项,取值时,指令只需取索引为3的long值即可。
下面就看个例子,好让大家对局部变量区有更深刻的认识。这个图来自《深入JVM》:
public static int runClassMethod(int i, long l, float f,
double d, Object o, byte b) {
return 0;
}
public int runInstanceMethod(char c, double d, short s,
boolean b) {
return 0;
}
runInstanceMethod的局部变量区第一项是个reference(引用),它指定的就是对象本身的引用,也就是我们常用的this,但是在runClassMethod方法中没这个引用,那是因为runClassMethod是个静态方法。
操作数栈和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。通过下面这个实例了解虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
int a= 100;
int b = 98;
int c = a+b;
1. begin
2. iload_0 // push the int in local variable 0 onto the stack
3. iload_1 // push the int in local variable 1 onto the stack
4. iadd // pop two ints, add them, push result
5. istore_2 // pop int, store into local variable 2
6. end
操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。
帧数据区:除了局部变量区和操作数栈外,jvm栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在jvm栈帧的帧数据区中。当JVM执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。除了处理常量池解析外,帧里的数据还要处理java方法的正常结束和异常终止。如果是通过return正常结束,则当前栈帧从jvm栈中弹出,恢复发起调用的方法的栈。如果方法有返回值,JVM会把返回值压入到发起调用方法的操作数栈。为了处理java方法中的异常情况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM给catch块中的代码。如果没发现,方法立即终止,然后JVM用帧区数据的信息恢复发起调用的方法的帧。然后再发起调用方法的上下文重新抛出同样的异常。
class Example3C{
public static void addAndPrint(){
double result = addTwoTypes(1,88.88);
System.out.println(result);
}
public static double addTwoTypes(int i,double d){
return i+d;
}
}
1.只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈
2 帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把返回结果存储在调用 方法的帧的操作数栈中
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。堆的内存模型大致为:
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。本人使用的是 JDK1.7,以下涉及的 JVM 默认值均以该版本为准。默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
在Java语言中,可作为GC Roots的对象包括下面几种:
在JDK1.2之后,Java对引用的概念进行的扩充,将引用分为强引用(Strong Reference)、软应用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这4中引用强度依次逐渐减弱:
对象在被回收之前要经历两次标记过程,如果发现对象经可达性分析检测,没有引用关联,它将会被标记并且进行筛选,筛选条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或finalize()方法已被虚拟机调用过,虚拟机认为这两种情况均为没有必要执行,对象将被回收,反之先执行finalize()后,收集对象,JVM并不保证finalize()一定成功被执行。
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
Parallel Old是ParallelScavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
CMS(ConcurrentMark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
①.初始标记(CMSinitial mark)
②.并发标记(CMSconcurrenr mark)
③.重新标记(CMSremark)
④.并发清除(CMSconcurrent sweep)
其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段要短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,其主要有三个显著缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“ConcurrentMode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
G1(GarbageFirst)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。
-XX:+
-XX:-
-XX:
-XX:
参数 |
描述 |
-XX:+UseSerialGC |
Jvm 运行在Client 模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收 |
-XX:+UseParNewGC |
打开此开关后,使用 ParNew + Serial Old 的收集器进行垃圾回收 |
-XX:+UseConcMarkSweepGC |
使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收, Serial Old 作为CMS 出现 “Concurrent Mode Failure” 失败后的后备收集器使用。 |
-XX:+UseParallelGC |
Jvm 运行在Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old的收集器组合进行回收 |
-XX:+UseParallelOldGC |
使用 Parallel Scavenge + Parallel Old 的收集器组合进行回收 |
-XX:SurvivorRatio |
新生代中 Eden 区域与Survivor 区域的容量比值,默认为 8 ,代表Eden:Subrvivor = 8:1 |
-XX:PretenureSizeThreshold |
直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 |
-XX:MaxTenuringThreshold |
晋升到老年代的对象年龄,每次 Minor GC 之后,年龄就加 1,当超过这个参数的值时进入老年代 |
-XX:UseAdaptiveSizePolicy |
动态调整 java 堆中各个区域的大小以及进入老年代的年龄 |
-XX:+HandlePromotionFailure |
是否允许 新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 |
-XX: ParallelGCThreads |
设置并行 GC 进行内存回收的线程数 |
-XX: GCTimeRatio |
GC 时间占总时间的比列,默认值为 99 ,即允许1% 的 GC时间,仅在使用Parallel Scavenge 收集器时有效 |
-XX: MaxGCPauseMillis |
设置 GC 的最大停顿时间,在 Parallel Scavenge 收集器下有效 |
-XX:CMSInitiatingOccupancyFraction |
设置 CMS 收集器在老年代空间被使用多少后出发垃圾收集,默认值为 68% ,仅在CMS 收集器时有效, -XX:CMSInitiatingOccupancyFraction=70 |
-XX:+UseCMSCompactAtFullCollection |
由于 CMS 收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在 CMS 收集器时有效 |
-XX:+CMSFullGCBeforeCompaction |
设置 CMS 收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与 UseCMSCompactAtFullCollection 参数一起使用 |
-XX:+UseFastAccessorMethods |
原始类型优化 |
-XX:+DisableExplicitGC |
是否关闭手动 System.gc |
-XX:+CMSParallelRemarkEnabled |
降低标记停顿 |
-XX:LargePageSizeInBytes |
内存页的大小不可设置过大,会影响 Perm 的大小,-XX:LargePageSizeInBytes=128m |
Client、Server模式默认GC |
新生代GC方式 |
老年代和持久代GC方式 |
Client |
Serial 串行GC |
Serial Old 串行GC |
Server |
Parallel Scavenge 并行回收GC |
Parallel Old 并行GC |
Sun/oracle JDK GC组合方式 |
新生代GC方式 |
老年代和持久代GC方式 |
-XX:+UseSerialGC |
Serial 串行GC |
Serial Old 串行GC |
-XX:+UseParallelGC |
Parallel Scavenge 并行回收GC |
Serial Old 并行GC |
-XX:+UseConcMarkSweepGC |
ParNew 并行GC |
CMS 并发GC |
-XX:+UseParNewGC |
ParNew 并行GC |
Serial Old 串行GC |
-XX:+UseParallelOldGC |
Parallel Scavenge 并行回收GC |
Parallel Old 并行GC |
-XX:+UseConcMarkSweepGC |
Serial 串行GC |
CMS 并发GC |
JVM栈 引用 http://blog.chinaunix.net/uid-9789791-id-3350479.html
GC收集器 引用 http://blog.csdn.net/java2000_wl/article/details/8030172