JVM自动内存管理:内存区域基础概念

一.Java虛拟机和Java内存区域概述

1.什么是虛拟机,什么是Java虛拟机

        虛拟机是指模拟某种计算机体系结构,执行特定指令集的软件。包括系统虛拟机(Virtual Box、VMware)、进程虛拟机。进程虛拟机有JVM、Adobe Flash Player、FC模拟器等,进程虛拟机可以划出一类高级语言虛拟机,如JVM、.NET CLR、P-Code等。

       Java语言虛拟机:可以执行Java语言的高级语言虛拟机。Java语言虛拟机并不一定就可以称为JVM,譬如:Apache Harmony(它可以正常运行Eclipse、Tomcat等)。

        Java虛拟机必须通过Java TCK(Technology Compatibility Kit)的兼容性测试的Java语言虛拟机才能称为”Java 虛拟机"。Java虛拟机并非一定要执行Java程序,业界三大商用JVM:Oracle HotSpot、Oracle JRockit VM、IBM J9 VM。

        Oracle HotSpot虛拟机,最初由名为“Longview Technologies"的小公司开发,后被Sun公司收购。最初并非面向Java语言开发,而是面向Strongtalk语言。

        HotSpot命名来自它的”热点代码探测“技术,它的热点代码探测能力可以通过执行计数器去找出最具有编译价值的代码,然后通知JIT以方法为单位对这些执行最为频繁的代码进行编译。如果一方法被频繁调用或方法中的有效循环次数很多,那它们就会分别触发标准编译和栈上替换编译动作,通过编译器和解释器的恰当配合可以在最优化的程序响应时间与最佳的执行效率之中取得完美的平衡,这样即时编译器的时间压力也会被相对降低,有助于编译器的研发人员引入更多的代码优化技术,输出更高质量的本地代码。

        HotSpot编译器是从JDK1.2开始加入Sun(Oracle) JDK,在JDK1.3开始成为Sun(Oracle) JDK的默认实现,在1.4中成为唯一的编译器。在2006年底开始开源,由此建立了Open JDK项目。

 

2.概念模型与具体实现

        公有设计,私有实现

        本课程涉及到的内存区域是在《Java虛拟机规范》(JVMS)中定义的概念模型,但JVMS也同时声明了这些概念不约束虛拟机的具体实现,只要求虛拟机实现的效果在外部看起来与规范描述的一致即可。

        举例,两者的联系和区别?比如在Java虛拟机规范中明确说明了Java堆这个运行时区域是需要进行自动内存管理的,它只是对一个效果的描述,简单来说我写代码,我使用内存,尽管用,不需要通过代码去释放我占用内存的对象,Java虛拟机会替我完成这一点,这是Java虛拟机所定义的效果,具体这个效果是怎么实现的,不同Java虛拟机会有不同的垃圾内存回收器,这些回收器各自的算法、效率都不一样。换句话说,就是只要能称之为Java虛拟机,通过了Java TCK认证,我们同样的JAVA代码在这些不同的Java虛拟机运行的结果都应该是一样的,但运行的快慢有可能有所区别。

 

3.Java虛拟机运行时数据区

        在《Java虛拟机规范》中定义了若干种程序运行期间会使用到的存储不同类型数据的区域。有一些区域是全局共享的,随着虛拟机启动而创建,随着虛拟机退出而销毁。有一些区域是线程私有的,随着线程开始和结束而创建和销毁。

        Java虛拟机运行时数据区域是所有Java虛拟机共同的内存区域概念模型。

        运行时数据区分为程序计数器、Java堆、Java虛拟机栈、本地方法栈、方法区。



4.程序计数器区域

        一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。通俗的说,可以把它看作一个指针,指着当前程序(字节码)正在运行的那一行代码。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虛拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。

        此内存区域是唯一一个在Java虛拟机规范中没有规定任何OutOfMemoryError情况的区域。

 

二.Java虛拟机栈和本地方法栈

1.Java虛拟机栈的概念和特征

        线程私有的内存区域,生命周期与线程相同,Java虛拟机栈描述的是Java方法执行时的内存概念模型。每个方法在执行时都会创建一个栈帧,用来创建这个方法的操作数栈、局部变量表、方法出口、动态链接的信息,每一个方法在调用和结束的过程,就对应了一个栈帧在虛拟机栈中入栈到出栈的过程。

        Java虛拟机栈是一个后进先出(LIFO)栈。

        存储栈帧,支撑Java方法的调用、执行和退出。

        可能出现OutOfMemoryError异常和StackOverflowError异常。如果线程请求的栈深度大于Java虛拟机所允许的最大深度,将出抛出StackOverflowError异常;如果Java虛拟机被设计成可以动态扩展,而动态扩展时又无法申请到足够的内存,将会抛出OutOfMemoryError异常。

 

2.本地方法栈的概念和特征

        本地方法栈是为了Java虛拟机执行Native方法所服务的,它也是一个线程私有的区域,一个后进先出(LIFO)的栈。

        作用是支撑Native方法的调用、执行和退出。

        可能出现OutOfMemoryError异常和StackOverflowError异常。

        有一些虛拟机(如HotSpot)将Java虛拟机和本地方法栈合并实现。

 

3.栈帧概念和特征

        Java虛拟机栈中存储的内容,它被用于存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。

        一个完整的栈帧包含:局部变量表、操作数栈、动态连接信息、方法正常完成和异常完成信息。

        局部变量表概念和特征:

        a.由若干个Slot组成,长度由编译期决定。局部变量表是一组变量值的存储空间,它用于存储方法、参数以及方法内部定义的局部变量等等,在Java编译器编译Class文件时,就在该方法的Code属性中确定了方法所需要局部变量表的最大容量。局部变量表的容量是以变量槽(Slot)为最小单位。

        b.单个Slot可以存储一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个Slot可以存储一个类型为long或double的数据。

        c.局部变量表用于方法间参数传递,以及方法执行过程中存储基础数据类型的值和对象的引用。

        操作数栈的概念和特征:

        a.是一个后进先出栈,由若干个Entry组成,长度由编译期决定。

        b.单个Entry即可以存储一个Java虛拟机中定义的任意数据类型的值,包括long和double类型,但是存储long和double类型的Entry深度为2,其他类型的深度为1。

        c.在方法执行过程中,栈帧用于存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

 

4.本地变量表和操作数栈实战

        通过一个具体例子来演示栈帧的局部变量表和操作数栈的工作方式。

public class Test {

	public int calc() {
		int a = 100;
		int b = 200;
		int c = 300;
		return (a+b)*c;
	}
}

        然后用cmd窗口,执行javap -verbose Test.class命令查看字节码:


        全部字节码输出如下:

Classfile /G:/devEnv/workspace/Test/bin/Test.class
  Last modified 2015-11-26; size 381 bytes
  MD5 checksum a5042df298b0ee7fbb6f0f47ab6ff20d
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // Test
   #2 = Utf8               Test
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LTest;
  #14 = Utf8               calc
  #15 = Utf8               ()I
  #16 = Utf8               a
  #17 = Utf8               I
  #18 = Utf8               b
  #19 = Utf8               c
  #20 = Utf8               SourceFile
  #21 = Utf8               Test.java
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>
":()V
         4: return
      LineNumberTable:
        line 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 7
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   LTest;
            3      14     1     a   I
            7      10     2     b   I
           11       6     3     c   I
}
SourceFile: "Test.java"

        具体分析如下:

        下图左边是上面生成的字节码指令,右边是我们将要用到的三个区域,分别是程序计数器、局部变量表、操作栈。程序在执行第一条字节码指令时,程序计数器指向了这条字节码指令的偏移量,偏移量为0,这条指令的名称是bipush,作用是把后面跟的一个整型数据参数入栈到操作数栈的栈顶,这条指令执行完之后,操作数栈的栈顶会存在数据100。

        接下来程序计数器指向了偏移量为2的指令istore_1,在这里,偏移量之所以增加了2,是因为前面的bipush指令中,指令占用了一个偏移量,它的参数也占用了一个偏移量。而这个istore_1指令只有指令本身,没有参数,所以这条指令只会占用一个偏移量。istore_1作用是把操作数栈的栈顶元素出栈,并将数据存储到局部变量表索引号为1的slot中。


        后面还有两组相同的指令,它们的作用是把200和300这两个数据入栈到操作数栈的栈顶,然后把这两个数据从操作数栈存储到局部变量表的2、3号slot中,当这三组指令都执行完毕之后,局部变量表的4个slot存储的内容分别是0号slot存储了this指针,1、2、3号slot分别存储了100、200、300。


        接下来执行偏移量为11的指令iload_1,它的作用是将局部变量表索引号为1的数据存储到操作数栈的栈顶,这条指令完成之后,操作数栈的栈顶的数据将会是100。下面将执行iload_2,它是把局部变量表索引号为2的数据存储到操作数栈的栈顶,当这条指令完成之后,操作数栈将会有两个元素,分别是栈顶元素200和第二个元素100。


        然后再执行指令iadd,它是一条整数加法指令,作用是把距离操作数栈栈顶最近的两个元素出栈相加,并将相加后的结果重新存入操作数栈栈顶。当这条指令执行之后,操作数栈的深度变回了1,并且存入了数据300。


        接着执行偏移量为14的指令iload_3,它将局部变量表索引号为3的数据300存入到操作数栈的栈顶。

        然后再执行整数乘法指令,将会把距离操作数栈最近的两个元素出栈相乘,然后把结果入栈。在这里即是把两个300出栈,然后把两个300相乘的结果90000重新入栈到操作数栈栈顶。


        最后一条指令ireturn作用是把操作数栈栈顶的数出栈,然后把这个值作为方法返回值进行返回。

5.内存异常实战

        Java虚拟机栈和本地方法栈可能发生如下异常情况:

        a.如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。

实例:

public class JavaVMStackSOF {

	private int stackLength = 1;
	
	public void stackLeak() {
		stackLength++;
		stackLeak();
	}
	
	public static void main(String[] args) throws Throwable {
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try {
			oom.stackLeak();
		}catch(Throwable e) {
			System.out.println("stack length:" + oom.stackLength);
			throw e;
		}
	}
}
        运行结果:递归深度是9576次
stack length:9576
Exception in thread "main" java.lang.StackOverflowError
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
...
        我们可以通过命令行参数可以限制HotSpot虚拟机每条线程的线程占用量大小,这个大小可以帮我们调节程序的递归深度,如:


        由于HotSpot虛拟机的本地方法栈和Java虛拟机栈是二合为一的,所以如上参数控制了Java虛拟机栈和本地方法栈两者的总大小,再次运行结果如下,递归深度减少到2273次:

stack length:2273
Exception in thread "main" java.lang.StackOverflowError
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:8)
...

        b.如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。

        还有一种情况就是,当Java虚拟机尝试建立新线程的时候,如果当前没有足够的内存去创建新线程,或者操作系统拒绝创建新的线程,这种情况下Java虚拟机也会抛出OutOfMemoryError异常。

        实例:它会不断的创建新的线程,直到达到操作系统的最大限制为止

public class JavaVMStackOOM {

	private void dontStop() {
		while(true) {
		}
	}
	
	public void stackLeakByThread() {
		while(true) {
			Thread thread = new Thread(new Runnable() {
				public void run() {
					dontStop();
				}
			});
			thread.start();
		}
	}
	
	public static void main(String[] args) throws Throwable {
		JavaVMStackOOM oom = new JavaVMStackOOM();
		oom.stackLeakByThread();
	}
}

        运行结果:操作系统很卡,直至JVM抛出OutOfMemoryError异常,然后退出。

 

三.Java堆

1.Java堆的概念

        Java堆是被所有Java线程共享的一块内存区域。

        Java堆是Java虛拟机中最大的一块内存区域,在Java虛拟机创建时创建,在Java虛拟机销毁时销毁。

        作用是作为Java对象的主要存储区域。

        JVMS明确要求该区域需要实现自动内存管理,即常说的GC,但并不限制采用哪种算法和技术去实现。

        可能出现OutOfMemoryError异常。

 

2.栈与堆

        栈与堆,从C/C++延伸而来的讨论。

        从栈到堆的关联过程:如下最简单的代码为例,在执行这段代码时,在JVM的本地变量表中必须有一个Slot容纳这个obj对象所代表的引用,所以这行代码的左边部分,对编译器的影响是会在Java虛拟机的本地变量表预留了这样一个Slot;而对于这行代码的右边这部分new Object(),当代码执行到这一行时,它将会在Java堆中产生一个Object变量,并且会把这个Object变量的reference赋值到JVM的本地变量表这个Object对象所对应的变量槽中,这个变量槽必须是存储一个reference类型的数据,而一个reference的数据它至少应该能完成两件事情,第一件事情是通过这个reference可以间接或直接的查找到这个对象的实例数据,第二件事情是通过这个reference可以直接或间接查找到这个对象的类型数据,那么要完成这样一件事情,JVM实现一般来说有两种典型的对象内存布局可以选择,第一种可以选择的方式是reference直接指向对象实例数据的地址,我们可以通过这个reference直接在Java堆中找到这个对象的实例数据,然后在这个对象的对象头之中预留一块指针,让VM可以通过对象头中的这个类型数据在方法区中找到这个对象所属的类型。


        另外一种可选的内存布局手段,reference并不直接指向对象的实例数据,而是指向一个在Java堆中的句柄池,一个句柄同时拥有到对象实例数据的指针以及到对象类型数据的指针。


        这两种对象访问和内存布局的方式是各有优势的,使用句柄来访问对象最大的好处就是reference中存储的是一个稳定的句柄地址,在对象被移动时只会改变句柄中实例数据的指针,而reference本身是不需要改变的。而第一种直接使用指针来访问对象的方式最大的好处是它的速度会更快,因为对比使用句柄它节省了一次指针定位的开销,这个reference就直接指向了对象实例数据,由于对象的访问在Java中是非常频繁的操作,这些开销积少成多也会成为一个很可观的执行成本。

        HotSpot虚拟机使用的是第一种直接指针来进行对象访问的,但是在整个软件开发范围来讲,各种语言、框架使有句柄池的习惯也十分普遍,甚至在JVM1.2之前的时候,JDK所带的VM使用的就是句柄访问方式。

 

3.Java堆内存异常实战

        Java堆可能发生如下异常情况:

        a.如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虛拟机将会抛出一个OutOfMemoryError异常。

        b.Java堆是出现内存异常概率最大的区域,如下为一个简单的内存溢出异常实例

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 通过上面的参数把Java堆限制在20m内,不允许扩展,也即是-Xms20m设置Java堆最小的范围20m,-Xmx20m设置Java堆最大的范围20m
 * -XX:+HeapDumpOnOutOfMemoryError作用是:当Java堆发生内存溢出时,Java虚拟机会自动把当前堆中的内存映像Dump到磁盘中,供后续的调试分析
 * 
 * @author bijian
 */
public class HeapOOM {

	static class OOMObject {
		
	}
	
	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<OOMObject>();
		while(true) {
			list.add(new OOMObject());
		}
	}
}

        运行参数设置:


        运行结果:出现OutOfMemoryError异常,并明确提示是Java heap space产生了异常。第二行的意思是它产生了这样的一个堆存储文件,可以看看虚拟机在发生内存溢出的那一刻的内存状态。第三行提示产生这个堆存储文件,耗时了0.326秒,并且这个堆存储文件大约有27M的大小。再往下是产生Java堆内存溢出时程序执行的堆栈。

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2476.hprof ...
Heap dump file created [26552604 bytes in 0.326 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at HeapOOM.main(HeapOOM.java:21)

 

四.方法区和运行时常量池

1.方法区概念

        方法区和Java堆一样,是Java线程的共享区域,作用是存储Java类的结构信息。实例数据是在类中定义的各种实例对象和他们的值,而类信息是指定义在Java代码中的常量、静态变量以及我们在类中所声明的各种方法、字段等等,还包括即时编译器编译之后产生的代码数据,在JVM规范之中,把方法区起了一个no heap(非堆)的别名,目的是与Java堆区分开来。

        JVMS不要求该区域实现自动内存管理,但是商用Java虛拟机都能够自动管理该区域的内存。

        方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。

 

2.运行时常量池的概念

        运行时常量池其实是Java方法区的一部分,也是一个全局共享的区域,作用是存储Java类文件常量池中的符号信息。Class文件除了有类的版本、字段、方法、接口等描述之外,还有一个重要的信息被称为常量池,用于存放编译器各种自变量和引用,这部分内容被类加载到JVM之后将会进入方法区的运行时常量池中存放。JVM规范对Class文件结构是有非常严格的规定的,Class文件之中的常量池以及其它信息必须保证每一个字节用于存储哪种数据,都符合VM规范上的要求才会被Java虛拟机所认可、装载和运行。但是对运行时常量池而言,JVM规范没有对这个区域作任何细节上的要求,不同产商所提供的Java虛拟机实现可以按照自已的需要来实现这个内存区域。

        不过,一般来说,运行时常量池除了保存Class文件之中描述的符号引用外,还会把这些符号翻译出来的直接引用也一起存储在这个常量池之中,运行时常量池相对于Class文件的常量池另外一个重要的特征是具备一定的动态性,Java语言并不要求常量一定只能在编译其间才能产生,也就是说运行时常量并非只有Class文件中常量池内容才是唯一的输入口,在运行期间可以通过代码产生新的常量并且将它们放至运行时常量池之中,这种特性被开发人员利用的最多的应该是String的intern方法。即然运行时常量池是方法区的一部分,它自然也会受到方法区的内存限制,当运行时常量池没有办法再申请到新的内存时将会抛出OutOfMemoryError异常。

 

3.HotSpot方法区实现的变迁

a.永久代与方法区

        在JDK1.2-JDK1.6,HotSpot使用永久代实现方法区。随着技术的发展,HotSpot使用永久代实现方法区的这种实现方式导致了许多关于Java的内存调试工具和错误诊断工具都需要对这种情况进行特殊处理,甚至没有办法使用在HotSpot之上,另外由Oracle同时收购了Sun和EA公司,获得了HotSpot和EA公司虛拟机的所有权,HotSpot使用永久代来实现方法区这种方式也导致了EA公司虛拟机的优秀特性和优秀工具往HotSpot移植时受到了极大的限制。

        因此,在JDK7开始,HotSpot开始了移除永久的代计划。

        a.符号表被移到Native Heap中

        b.字符串常量和类的静态引用被移到Java Heap中

        在JDK8开始,永久代已被元空间(Metaspace)所代替,元空间是实现在Java虛拟机本身的Native Heap中的,HotSpot方法区迁移过程除了对内存溢出时选用的调试工具、调试手段产生影响之外,对Java代码的运行也会产生一些很轻微的变化。

 

b.方法区内存异常实战

        演示永久代变迁过程导致的异常差异。

实例一:

        String.intern()方法作用是,虚拟机维护了这样的一个字符串池,如果字符串池中有这个字符串常量,那么intern方法将会返回池中这个常量的地址,如果这个字符串池中没有所需的常量,虚拟机会把这个字符串先加到字符串池之中,然后再返回字符串池中的地址。

   HotSpot实现方法区变迁的时候,其中一点提到了从JDK7开始,字符串常量和类的静态引用被移到了java Heap中,这一点对这个实例代码会产生什么样的影响呢?

public class RuntimeConstantPoolChange {

	public static void main(String[] args) {
		
		String str1 = new StringBuilder("深圳").append("欢迎您").toString();
		System.out.println(str1.intern() == str1);
		
		String str2 = new StringBuilder("ja").append("va").toString();
		System.out.println(str2.intern() == str2);
	}
}

        在JDK1.6上运行的结果:

false
false

        由于JDK6及之前版本,字符串常量池是位于方法区之中的,也就意味着这两个String的intern方法,返回的都是位于方法区中的内存地址,而str1、str2必然是处于Java堆中的内存地址,两个内存地址必然不会相等。

        在JDK7、JDK8上运行的结果:

true
false
        第一行代码产生一个“深圳欢迎您”的字符串,这个字符串在JVM中是第一次出现,str1.intern执行时,在字符串常量池中并不存在一个叫“深圳欢迎您”的常量,按照这个intern的解释,它会把“深圳欢迎您”这个常量放到常量池之中,然后返回这个常量池的地址,在JDK7中,常量池已被挪至Java堆中,这时返回来的就是Java堆中对象的地址,也即字符串str1的地址,所以第一个返回true。而第二个输出返回了false,它的原因是str2产生的字符串是“java“,“java“这个字符串在执行到这段代码时,已在常量池之中了,intern并没有把str2这个地址放到字符串常量池之中,这时它返回的这个地址和str2字符串的地址必须不相等。

        分析字节码如下:


        我们将上面代码改成如下:

public class RuntimeConstantPoolChange {

	public static void main(String[] args) {
		
		String str1 = new StringBuilder("深圳").append("欢迎您").toString();
		System.out.println(str1.intern() == str1);
		
		String str2 = new StringBuilder("深").append("圳").toString();
		System.out.println(str2.intern() == str2);
	}
}
        运行结果:
true
false
        说明“深圳”这个字符串在执行String str2 = new StringBuilder("深").append("圳").toString();代码执行之前进入了运行时常量池之中。

实例二:

        这是一个运行时常量池溢出的一段演示,这段代码我们会通过-XX:PermSize=10M -XX:MaxPermSize=10M把HotSpot JVM的永久代限制为10M大小。

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author bijian
 *
 */
public class RuntimeConstantPoolOOM {

	public static void main(String[] args) {
		//使用List保持着常量池引用,避免Full GC回收常量池行为
		List<String> list = new ArrayList<String>();
		//10MB的PermSize在integer范围内足够产生OOM了
		int i = 0;
		while(true) {
			list.add(String.valueOf(i++).intern());
		}
	}
}
        JDK6运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:17)
        JDK7或JDK8运行果不会产生内存溢出。原因分析:我们知道,int的取值范围是2的31次方到负的2的31次方,这段代码可以产生那么多不同的、独立的字符串,这些字符串在10M的范围内足够能撑满永久代,让永久代产生内存溢出,但是在JDK7或JDK8中,已经把字符串常量池挪到了java heap中,这么多的对象已经不足够让java heap产出内存溢出。这里虽然是一个无限循环,但它产生的字符串对象是有限的,因为它不能超过int类型的取值范围,所以这段代码在JDK1.7或JDK1.8中能完整的执行下去。

五.直接内存

1.直接内存的概念和特征

        直接内存并非JVMS定义的标准Java运行时内存区域。

        随JDK1.4中加入的NIO被引入,目的是避免在Java堆和Native堆中来回复制数据带来的性能损耗。NIO类引入了一种基于通道和缓存区的IO方式,它可以使用Native的函数库直接对外分配内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的一个引用来进行操作,这样的好处是在一些场景之中能显著提高性能,因主它避免了Java堆和Native堆中来回复制数据。

        直接内存是被Java虚拟机所有线程共享的一个内存区域,它能被Java虚拟机自动管理,但是在检测手段上可能会有一些简陋。

        直接内存的分配不会受Java堆大小的限制,但会受本机总内存大小以及处理器寻址空间的限制,服务器管理员在给Java虚拟机配置参数时会根据实际内存设置Java堆大小等信息,但是经常会忽略掉直接内存的大小,这样在一些极端情况下将会导致Java虚拟机的各个内存区域总和大于物理内存的限制,从而导致一些区域进行动态扩展时Java虚拟机报出OutOfMemoryError异常。

 

2.直接内存异常实战

        演示直接内存所导致的内存溢出异常。

        如下代码是通过反射手段拿到Unsafe这个类allocateMemory方法的访问权,Java虚拟机的设计者并不建议大家在外部去调用它,这里为了演示,使用反射手段来绕开Java虚拟机的这个限制,直接调用allocateMemory方法,这里并没有直接的DirectMemory或才DirectByteBuffer这样的类出现,但是在DirectByteBuffer里面分配内存确实使用了Unsafe.allocateMemory方法,因此在这里直接使用这个方法演示,效果是一样的。

import java.lang.reflect.Field;

import sun.misc.Unsafe;

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * @author bijian
 */
public class DirectMemoryOOM {

	private static final int _1MB = 1024 * 1024;
	
	public static void main(String[] args) throws Exception {
		Field unsafeField = Unsafe.class.getDeclaredFields()[0];
		unsafeField.setAccessible(true);
		Unsafe unsafe = (Unsafe) unsafeField.get(null);
		while(true) {
			unsafe.allocateMemory(_1MB);
		}
	}
}
        运行结果:产生了OutOfMemoryError异常,并且没有明确提示产生异常的区域。由直接内存导致的内存溢出,另外一个明显的特征是在Heap Dump的存储文件当中不会看见有明显的异常存在。
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at DirectMemoryOOM.main(DirectMemoryOOM.java:19)

学习视频地址:http://www.jikexueyuan.com/course/1793_1.html?ss=1

猜你喜欢

转载自bijian1013.iteye.com/blog/2259284