原创

JVM内存区域与垃圾收集学习笔记(参考《深入理解JAVA虚拟机》)

一、JVM框架

Java虚拟机HotSpot的框架:


JVM主要组成部分:

Class Loader(类加载器)、Runtime Data Area(运行时数据区)、执行引擎(Execution Engine)。

二、Java内存区域

1. 程序计数器

程序计数器(Program CounterRegister) 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器. 在虚拟机的概念模型里, 字节码解释器工作时就是通过改变这个计数器的值来选去吓一跳需要执行的字节码指令, 分支, 循环, 跳转, 异常处理, 线程恢复等基础功能都需要依赖这个计数器来完成.

2. Java虚拟机栈

与程序计数器一样, Java虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的, 它的生命周期与线程相同. 虚拟机栈描述的是Java方法执行的内存模型: 每个方法被执行的时候都会同时创建一个栈帧(Stack Frame) 用于存储局部变量表, 操作栈, 动态链接, 方法出口等信息. 每一个方法被调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程.会抛两种异常:

2.1.如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常;

2.2.如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态拓展, 只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当拓展时无法申请到足够的内存时会抛出OutOfMemoryEoor异常.

3.本地方法栈

本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务, 而本地方法栈则是为虚拟机使用到Native方法服务. 虚拟机规范中对本地方法栈中的方法使用的语言, 使用方式与数据结构并没有强制规定, 因此具体的虚拟机可以自由实现它. 甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一. 与虚拟机栈一样, 本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常.

4.Java

对于大多数应用来说, Java(Java Heap) Java虚拟机所管理的内存中最大的一块. Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建. 此内存区域的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存.

当前主流的虚拟机都是按照可拓展来实现的( 通过-Xms 初始化堆, -Xmx 最大堆空间), 如果在堆中没有内存完成实例分配, 并且堆也无法在拓展时, 将会抛出OutOfMemoryError异常.

5. 方法区

方法区(Method Area) Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap非堆, 目的应该是与Java Heap 区分开来.

6.运行时常量池

运行时常量池(Runtime Constant Pool) 是方法区的一部分. Class文件中除了有类的版本, 字段,方法, 接口等描述信息外, 还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后存放到方法区的运行时常量池中.

7. 直接内存

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 但是这部分内存也被频繁地使用, 而且也可能导致OutOfMemoryError异常出现. 显然, 本机直接内存的分配不会受到Java堆大小的限制, 但是, 既然是内存, 则肯定还是会受到本机总内存的大小及处理器寻址空间的限制. 服务器管理员配置虚拟机参数时, 一般会根据实际内存-Xmx等参数信息, 但经常会忽略到直接内存, 使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制), 从而导致动态扩展时出现OutOfMemoryError异常.

 

常见异常示例

package oom;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import sun.misc.Unsafe;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class OOMTest {

public static void main(String[] args) {

javaMethodAreaOOM();



}



/**

* 堆溢出

*/

public static void heapOOMTest() {

ArrayList list = new ArrayList();

while (true) {

list.add(new OOMTest());

}

}



private static int stackLength = 1;



/**
* 虚拟机栈溢出
*/
public static void jvmStackOOMTest() {
stackLength++;
jvmStackOOMTest();



}



/**

* 本地方法栈溢出,win系统不要执行,演示一下容易雪崩

*/

public static void nativeStackOOMTest() {

while (true) {

Thread t = new Thread(new Runnable() {

@Override

public void run() {

dontStop();

}

});

t.start();

}

}



private static void dontStop() {

while (true) {

}

}



/**

* 运行时常量池溢出 目前只再jdk1.7以前会报错,之后不会溢出

*/

public static void runTimeConstantPoolOOM() {

List<String> list = new ArrayList<String>();

int i = 0;

while (true) {

list.add(String.valueOf(i++).intern());

}

}



/**

* 使用cglib方法去出现内存溢出

*/

public static void javaMethodAreaOOM() {

while (true) {

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(OOMObject.class);

enhancer.setUseCache(false);

enhancer.setCallback(new MethodInterceptor() {

@Override

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {



return proxy.invokeSuper(obj, args);

}

});

enhancer.create();

}

}



static class OOMObject {



}



private static final int _1MB = 1024 * 1024;

/**

* 本机内存溢出 注意需要引用 import sun.misc.Unsafe;

* @throws Exception

* @throws IllegalArgumentException

*/

public static void directMemoryOOM() throws IllegalArgumentException, Exception {

Field unsafeFiled = Unsafe.class.getDeclaredFields()[0];

unsafeFiled.setAccessible(true);

Unsafe unsafe = (Unsafe)unsafeFiled.get(null);

while (true) {

unsafe.allocateMemory(_1MB);

}

}

}

 

三、虚拟机垃圾算法

Hotspot JVM中,大致有以下四种垃圾收集算法: 

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

它是最基础的算法,分为2个阶段标记清除:首先标记出所有需要回收的对象,在标记完后统一回收掉所有被标的对象。之所以说它是最基础的收集算法,是因为后续的搜集算法都是基于这种思路并改进其缺点而来的。 
 标记-清除算法主要有两个缺点:1、标记和清除的效率都不高;2、会产生许多内存速碎片,内存碎片会导致在运行时保分配较大的对象时,由于无法找到连续的内存而不得不提前触发一次垃圾收集操作。 
 
                                           “标记-清除(Mark-Sweep)”算法示意图 

复制(copying)算法

该算法将JVM可用的内存按容量划分为对等的两块,每次只使用其中的一块,当其中一块使用完后,就将还存活的对象拷贝到另外一块上,然后把已使用过的内存一次清空。 
  复制算法优点是实现简单,效率高,但是由于每次只使用一半的内存空间,这就会浪费掉一般的内存容量。 
另外,复制算法在对象存活率较高的情况下要执行较多的复制操作,效率会变低。 

标记-整理(mark-compact)算法

前期标记过程和标记-清除(Mark-Sweep)”算法一样,但后续操作不是直接清除可回收对象,而是让所有存活的对象都向内存的一端移动,然后直接清理掉边界以外的内存。其示意图如下: 
 
                          “标记-整理(mark-compact)”算法示意图 

分代收集算法

当前JAVA商业虚拟机的垃圾收集都是采用分代收集算法,该苏算法主要根据对象的存活周期的不同将内存划分为几块。一般是把JAVA的堆分为新生代和老年代,这样就可以根据各个年代的特点采用最恰当的手机算法。新生代中的对象大都朝生夕死,只有少量的存活,这样就可以选择复制算法;而老年代中对象因为存活率高、没有额外空间担保,就必须使用标记-清除标记-整理算法来回收。 

Young Generationeden区分配新对象,eden区满后,发生一次minor garbage collection,把eden区和一个survivor区中存活的对象移动到另一个survivor区中,存活的对象age1,当存活的对象age达到一个阈值时晋升到Old Generation

Old Generation保存存活长久对象的地方,Old Generation满后会发生major garbage collectionfull garbage collection)。

Stop the World Eventminor garbage collectionmajor garbage collection都是Stop the World Event,即垃圾回收的时候会暂停程序中线程的执行。

Permanent generation保存JVM中用于描述类和方法的元数据信息。


Hotspot JVM 1.6的垃圾收集器种类示意图 (图中两个收集器之间有连线,说明它们可以配合使用)

 

四、虚拟机垃圾收集器

Serial垃圾收集器:单线程的垃圾收集器,适用于新生代。在触发该收集器时,必须暂停其他所有工作线程(该事件被SUN成为“Stop The World”),直到它收集结束“Stop The World”会给用户代码很不好的用户体验。

ParNew垃圾收集器:Serial收集器的多线程版本,除了使用多个线程进行垃圾回收外,其他行为和Serial收集器完全一样,适用于新生代。

 Parallel Scavenge垃圾收集器:该收集器是使用复制算法、适用于新生代的垃圾收集器,其目的是达到一个可控制的吞吐量(Throughput),从而最高效的利用CPU时间,尽快的完成任务,主要适合在后台运算而不需要太多交互的任务场景。 

Parallel Scavenge垃圾收集器主要提供两个参数用于精确控制吞吐量,分别是 控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis和直接设置吞吐量大小的 -XX:GCTimeRatio 
除上述两个参数外,还有一个开关参数-XX:+UseAdaptiveSizePolicy,当设置这个参数后,既不需要手工指定新生代的大小等参数了,虚拟机会根据当前系统的允许情况动态调整这些参数。 由于与吞吐量关系密切,Parallel Scavenge也被称为吞吐量优先的垃圾收集器。

 Serial Old垃圾收集器:老年代的单线程垃圾收集器。另外一个作用就是在CMS并发收集器发生Concurrent Mode Failure时使用。

 Parallel Old垃圾收集器:Parallel Scavenge收集器的老年代的版本,使用多线程和标记-整理算法。该收集器是在JDK1.6中才有的。 

在注重吞吐量和CPU资源敏感的场景中,应首选Parallel Scavenge + Parallel Old的组合。

 CMS垃圾收集器:是以用户响应优先的并发垃圾收集器,基于标记-清楚算法实现的。 该收集器执行垃圾回收时不会暂停其他工作线程,即不会发生“Stop The World”,它每次只清除部分垃圾;由于CMS是基于标记清除算法实现的,故会产生大量内存碎片,这会给大对象分配带来大麻烦;于是CMS提供了一个 -XX:+UseCMSCompactAtFullCollection的开关参数,该参数会在每次触发一次Full GC后执行一个碎片整理操作,内存整理无法并发操作,于是空间碎片没有了但停顿时间不得不变长,然后就有了另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后执行一次带压缩的Full GC 

在注重用户响应体验的场景中,应首选ParNew + CMS的组合。

G1收集器

为解决CMS算法产生空间碎片和其它一系列的问题缺陷,HotSpot提供了另外一种垃圾回收策略,G1Garbage First)算法,通过参数-XX:+UseG1GC来启用,该算法在JDK 7u4版本被正式推出,G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,竟可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:

l 垃圾收集线程和应用线程并发执行,和CMS一样

l 空闲内存压缩时避免冗长的暂停时间

l 应用需要更多可预测的GC暂停时间

l 不希望牺牲太多的吞吐性能

对于垃圾收集器的组合使用可以通过下表中的参数指定:


正文到此结束
本文目录