Java内存区域与OutOfMemoryError

运行时数据区域

JVM在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,各区域有各自的用途以及生命周期(创建和销毁)。

运行时数据区


接下来我们针对JVM各内存区域来拆分介绍各自的用途。

程序计数器

程序计数器(Program Counter Register) 是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

CPU处理任务时并不是一直处理当前的任务,而是通过给每个线程分配CPU时间片,如果当前线程处理的时间超过分配的时间片,就切换下一个线程,时间片极短,所以为了线程切换后恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的计数器独立存储,所以这块内存区域为线程私有

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的JVM字节码执行的地址;如果正在执行的是Native方法,这个计数器值则应为空。此内存区域是唯一一个在《Java虚拟机规范》没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

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

局部变量表存放了编译器可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄和其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储控件单位以变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间也不会改变局部变量表的大小(变量槽的数量)。

《Java虚拟机规范》中,对这个内存区域规定了两类异常的情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常(HotSpot虚拟机的栈容量是不可以动态扩展的,只要线程申请栈空间成功就不会OOM)。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈和虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是虚拟机使用到的本地(Native)方法服务,也是线程私有的,它的生命周期与线程相同。

Java堆

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。

Java堆是垃圾收集器管理的内存区域,它也被称作“GC堆”,现代垃圾都集齐大部分都是基于分代收集理论设计的,所以Java堆常会出现新生代(Young Gen)(其中包含EdenSurvivor)、老年代(Tenured Gen)永久代(Perm Gen)等名词。

所有线程共享的Java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。根据《Java虚拟机规范》,Java堆可以处于物理上不连续的内存。

Java堆可以被实现成固定大小的,也可以试可扩展的,可以通过参数-Xms(初始堆大小)和-Xmx(最大堆大小)来设定。若Java堆中没有内存去完成实例分配,并且堆也无法再扩展时,JVM将会抛出OutOfMemoryError异常。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

在JDK 8前,很多人把方法区与永久代混为一谈,实际上只是使用永久代来实现方法区而已,这样做是为了让HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,剩下专门为方法区编写内存管理代码的工作。但是这将会导致Java应用更容易遇到内存溢出的问题。 JDK 7时,HotSpot已经把原本放在永久代的字符串常量池,静态变量移出;JDK 8时,完全放弃了永久代的概念,改用了JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK 7中永久代还剩余的内容(类型信息)全部移到了元空间中。
《Java虚拟机规范》对方法去的约束是非常宽松的,和Java对一样,不需要连续的内存可以选择固定大小或可拓展以外,甚至不需要实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样永久“存在”了。这区域的内存回收目标主要针对常量池的回收和对类型的卸载。
根据《Java虚拟机规范》,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

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

运行时常量池对于Class文件常量池的另一个重要特征就是具备动态性,Java语言并不要求常量一定只有编译器才产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。

比如String.intern()就具有这种特性,intern()的描述:如果字符串常量池中已经包含了由equals()方法确定的与该字符串对象相等的字符串,则返回池中的字符串。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用

/**
 * JDK 8的基础上运行
 */
public class StringInternTest {
    public static void main(String[] args) {
        String a = "te";
        String b = "st";
        String c = "test";
        String d = "test";
        String e = a + b;
        String f = new String(c);
        // c = "test"时先查找常量池中是否有“test”字符串,
        // 若没有则将“test”字符串添加到常量池,栈帧中中的c指向常量池中的“test”
        // c和d都指向常量池“test”的引用
        System.out.println("c == d :" + (c == d));
        // f = new String(c);是在Java堆上创建String对象
        System.out.println("c == f :" + (c == f));
        // e = a + b,JVM会把它处理为StringBuilder.append()的方式拼接字符串
        System.out.println("c == e :" + (c == e));
        System.out.println("-------------------------------");
        // 如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串。
        // 否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。
        System.out.println("c == d :" + (c.intern() == d.intern()));
        System.out.println("c == f :" + (c.intern() == f.intern()));
        System.out.println("c == e :" + (c.intern() == e.intern()));
    }
}

输出结果如下

c == d :true
c == f :false
c == e :false
-------------------------------
c == d :true
c == f :true
c == e :true

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但这部分内存也被频繁使用,而且也可能会导致OutOfMemoryError异常的出现。

在JDK 1.4新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。

直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存大小及处理器寻址空间的限制。一般配置虚拟机参数时,会根据实际内存取设置-Xmx等参数信息,忽略掉直接内存,是的各内存区域的总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

HotSpot虚拟机对象探秘

上一节只是简单觉介绍了运行时数据区域,只是大致明白Java虚拟机内存模型的概况。这一节,我们来共同学习HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。

对象的创建

Java程序在运行过程中都有对象被创建,创建对象通常是一个new关键字而已,而虚拟机中对象的创建又是怎样一个过程呢?

检查加载

当Java虚拟机遇到一条字节码new指令时:首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有那必须先执行相应类的加载过程。

分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以确定,所以为对象分配空间的任务实际上就是把一块确定大小的内存块从Java堆中划分出来。

若Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞(Bump The Pointer);
但如果Java堆中的内存不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例并更新列表上的记录,这种分配方式称为空闲列表(Free List)

对象创建在虚拟机中是非常频繁的行为,即使仅修改了一个指针所指向的位置,在并发的情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没修改,对象B又同时使用了原来指针来分配内存的情况。这个问题有两种解决方案:一种是对分配内存空间的动作进行同步处理,实际上虚拟机采用CAS加失败重试的方式保证更新操作的原子性;另一种则是内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),那个线程要分配内存,就在那个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配的缓存区时才需要同步锁定。

初始化为零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这步保证了对象实例化字段在Java中可以不赋初始值就可以直接使用,使程序能够访问到这些字段的数据类型对应的零值。

对象设置

Java虚拟机还要对对象进行必要的设置,例如:对象是哪个类的实例、如何才能找到类的原数据信息、对象的哈希码(滴啊搜用hashCode()时才会计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

构造函数

经过上面几步,一个对象已经产生,但Class文件中的()还没有执行,所有字段还是默认的零值。new指令之后会接着执行(),按照代码指定对对象进行初始化,一个真正可用的对象才算完全构造出来。

HotSpot代码片段

// 确保常量池中存放的是已解释的类
if (!constants->tag_at(index).is_unresolved_klass()) {
	// 断言确保是klassOop和instanceKlassOop(这部分下一节介绍)
	oop entry = (klassOop) *constants->obj_at_addr(index);
	assert(entry->is_klass(), "Should be resolved klass");
	klassOop k_entry = (klassOop) entry;
	assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass");
	instanceKlass* ik = (instanceKlass*) k_entry->klass_part();
	// 确保对象所属类型已经经过初始化阶段
	if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
		// 取对象长度
		size_t obj_size = ik->size_helper();
		oop result = NULL;
		// 记录是否需要将对象所有字段置零值
		bool need_zero = !ZeroTLAB;
		// 是否在TLAB中分配对象
		if (UseTLAB) {
			result = (oop) THREAD->tlab().allocate(obj_size);
		}
		if (result == NULL) {
			need_zero = true;
			// 直接在eden中分配对象
			retry:
			HeapWord* compare_to = *Universe::heap()->top_addr();
			HeapWord* new_top = compare_to + obj_size;
			// cmpxchg是x86中的CAS指令, 这里是一个C++方法, 通过CAS方式分配空间, 并发失败的话, 转到retry中重试直至成功分配为止
			if (new_top <= *Universe::heap()->end_addr()) {
				if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
					goto retry;
				}
				result = (oop) compare_to;
			}
		}
		if (result != NULL) {
			// 如果需要, 为对象初始化零值
			if (need_zero ) {
				HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
				obj_size -= sizeof(oopDesc) / oopSize;
				if (obj_size > 0 ) {
					memset(to_zero, 0, obj_size * HeapWordSize);
				}
			}
			// 根据是否启用偏向锁, 设置对象头信息
			if (UseBiasedLocking) {
				result->set_mark(ik->prototype_header());
			} else {
				result->set_mark(markOopDesc::prototype());
			}
			result->set_klass_gap(0);
			result->set_klass(k_entry);
			// 将对象引用入栈, 继续执行下一条指令
			SET_STACK_OBJECT(result, 0);
			UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
		}
	}
}

对象的访问定位

创建对象后,在使用该对象时,会通过栈上的reference数据来操作堆上对应的对象。
reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有以下两种:

  • 如果使用句柄访问,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。

通过句柄访问对象

  • 如果使用直接指针访问,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。
    通过直接指针访问对象

HostSpot主要是用第二种方式进行访问,是用直接指针来访问,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项执行成本的开支。

OutOfMemoryError异常

《Java虚拟机规范》中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有可能发生OutMemoryError异常。

C:\>java -version
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)

Java堆溢出

Java堆用于存储对象实例,如果不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出的异常。

在以上虚拟机配置的基础上添加-XX:+HeapDumpOnOutOfMemoryError,它的作用是当堆抛出OOM错误时,转存出当前的内存堆转储快照,添加配置-XX:HeapDumpPath会在指定目录下转存出当前的内存堆转储快照;

public class HeapOOM {
    /**
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/Java/dump
     * 
     * -verbose:gc:控制台输出GC情况
     * -Xms:初始堆大小
     * -Xmx:程序运行期间堆的最大内存
     * -Xmn:设置年轻代大小
     * -XX:+PrintGCDetails:控制台输出GC详细的信息
     * -XX:SurvivorRatio:(Eden:FromSurvivor:ToSurvivor = 8:1:1)
     */
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new Object());
        }
    }
}
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:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at oom.HeapOOM.main(HeapOOM.java:12)

从以上打印,可以看到java.lang.OutOfMemoryError: Java heap space。要解决及分析堆内存区域的异常,我们可以通过Eclipse Memory Analyzer对Dump出来的堆转储快照进行分析。应首先确认内存中导致OOM的对象,进而确定是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。
堆转储快照进行分析

堆转储快照进行分析
如果是内存泄露可以通过工具查看对象到GC Root的引用链,找到泄露对象通过什么样的引用路径与那些GC Root相关联才导致GC无法回收它们,根据泄露对象的类型信息以及它到GC Roots引用链的信息可以准确定位到这些对象的创建未知,进而找出产生内存泄露的代码位置。

如果是内存溢出。就应当检查Java虚拟机的堆参数设置,设置的同时结合机器内存判断是否存在调整空间,再从代码分析哪些对象的生命周期、持有时间过长,存储结构设计不合理的情况。

虚拟机栈和本地方法栈溢出

HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以栈的容量只通过-Xss来设定。

关于虚拟机栈与本地方法栈,《Java虚拟机规范》中描述了两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

HtoSpot虚拟机不支持动态扩展,除非在创建线程申请内存时就无法获得足够内存而出现OutOfMemoryError异常,所以线程运行时不会因为扩展而导致内存溢出,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。

场景一:使用-Xss参数减少栈内存容量。
结果:抛出StackOverflowError,异常出现时输出的堆栈深度相应缩小。

/**
 * 
 * 修改JVM配置:-Xss128k
 */
public class StackSOE {
    private static int count;

    static void recursionAddCount() {
        count++;
        recursionAddCount();
    }

    public static void main(String[] args) {
        try {
            recursionAddCount();
        } catch (StackOverflowError e) {
            System.out.println("count = " + count);
            e.printStackTrace();
        }
    }
}
count = 1684
java.lang.StackOverflowError
	at demo.StackSOE.recursionAddCount(StackSOE.java:11)
	at demo.StackSOE.recursionAddCount(StackSOE.java:11)
	at demo.StackSOE.recursionAddCount(StackSOE.java:11)
	···

场景二:定义大量的本地变量,增大方法帧中本地变量表的长度。
结果:抛出StackOverflowError异常,异常出现时输出的堆栈深度相应缩小。

public class StackSOE {
    private static int count;
    public static void recursionAddCount(){
        double d1,d2,d3,d4,d5,d6,d7,d8,d9,d10,
                d11,d12,d13,d14,d15,d16,d17,d18,d19,d20,
                d21,d22,d23,d24,d25,d26,d27,d28,d29,d30,
                d31,d32,d33,d34,d35,d36,d37,d38,d39,d40,
                d41,d42,d43,d44,d45,d46,d47,d48,d49,d50,
                d51,d52,d53,d54,d55,d56,d57,d58,d59,d60,
                d61,d62,d63,d64,d65,d66,d67,d68,d69,d70,
                d71,d72,d73,d74,d75,d76,d77,d78,d79,d80,
                d81,d82,d83,d84,d85,d86,d87,d88,d89,d90,
                d91,d92,d93,d94,d95,d96,d97,d98,d99;
        count ++;
        recursionAddCount();
        d1=d2=d3=d4=d5=d6=d7=d8=d9=d10=
        d11=d12=d13=d14=d15=d16=d17=d18=d19=d20=
        d21=d22=d23=d24=d25=d26=d27=d28=d29=d30=
        d31=d32=d33=d34=d35=d36=d37=d38=d39=d40=
        d41=d42=d43=d44=d45=d46=d47=d48=d49=d50=
        d51=d52=d53=d54=d55=d56=d57=d58=d59=d60=
        d61=d62=d63=d64=d65=d66=d67=d68=d69=d70=
        d71=d72=d73=d74=d75=d76=d77=d78=d79=d80=
        d81=d82=d83=d84=d85=d86=d87=d88=d89=d90=
        d91=d92=d93=d94=d95=d96=d97=d98=d99=0.0d;
    }

    public static void main(String[] args) {
       try {
           recursionAddCount();
       }catch (StackOverflowError e){
           System.out.println("count = " + count);
           e.printStackTrace();
       }
    }
}
count = 4891
java.lang.StackOverflowError
	at demo.StackSOE.recursionAddCount(StackSOE.java:19)
	at demo.StackSOE.recursionAddCount(StackSOE.java:19)
	at demo.StackSOE.recursionAddCount(StackSOE.java:19)
	···

结论:当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。

方法区和运行时常量池溢出

方法区的主要职责用于存放类型的相关信息,比如类名、访问修饰符、常量池、字段描述、方法描述等。

JDK 6

JDK 6中,由于运行时常量池是方法区(永久代)的一部分,所以这两个区域的测试可以放在一起进行。
String.intern()前面已经介绍过,它是一个native方法,作用:如果字符串常量池中已经包含了由equals()方法确定的与该字符串对象相等的字符串,则返回池中的字符串。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。

C:\>java -version
java version "1.6.0_43"
Java(TM) SE Runtime Environment (build 1.6.0_43-b01)
Java HotSpot(TM) 64-Bit Server VM (build 20.14-b01, mixed mode)
/**
 * 修改JVM配置:-XX:PermSize=6M -XX:MaxPermSize=6M
 * -XX:PermSize:设置持久代(perm gen)初始值
 * -XX:MaxPermSize:设置持久代最大值
 */
public class JDK6ConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at demo.JDK6ConstantPoolOOM.main(JDK6ConstantPoolOOM.java:12)

异常信息如上所示,说明的常量池属于方法区的一部分,也就是JDK 6 HotSpot虚拟机中的永久代。

JDK 7

使用JDK 7及以上版本修改JVM配置后(-XX:PermSize=6M -XX:MaxPermSize=6M),执行该段代码都不会得到JDK 6执行结果。原因是因为JDK7起,存在在永久代的字符串常量池被移至Java堆中。
与此同时,在JDK6与JDK7的版本下调用String.intern()去判断String字符串是否相等会返回不同的结果。

public class StringIntern {
    public static void main(String[] args) {
        String str1 = new StringBuilder("te").append("st").toString();
        System.out.println(str1.intern() == str1);
        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

JDK 6运行结果如下:

false
false

原因:JDK 6中,intern()会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的就是永久代中字符串示例的引用。通过StringBuilder创建的字符串对象实例在Java堆,所以返回false。

JDK 7运行结果如下:

true
false

原因:JDK 7中,intern()首先从字符串常量池中获取由equals()方法确定与该字符串对象相等的字符串并返回,所以StringBuilder.toString(),返回的字符串在Java堆中的常量值中已经存在,所以为true。至于第二个判断str2.intern() == str2 = false,是因为“java”字符串在我们StringBuilder.toString之前,常量池中已经存在它的引用,所以为false,具体原因如下所示:

public class Version {
    private static final String launcher_name = "java";
    private static final String java_version = "1.8.0_152";
    private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
    private static final String java_profile_name = "";
    private static final String java_runtime_version = "1.8.0_152-b16";
    ···
}

对于方法区的内存溢出测试,我们可以基于动态代理来实现。

C:\>java -version
java version "1.7.0_67"
Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * -XX:PermSize=100M -XX:MaxPermSize=100M
 */
public class JDK7MethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(JDK7MethodAreaOOM.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }

    }
}
Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

工具使用的是JProfiler,如下图所示:

内存
关于动态代理Spring、Hibernate都会使用到动态代理,当增强的类越多,就需要给方法区足够的内存以保证生成的新类型可以载入。

JDK 8

JDK 7将JDK 6的字符串常量池从永久代转移至Java堆中,在JDK 8后,永久代完全退出了历史舞台,元空间作为替代者登场。

前面通过动态代理等动态创建新类型使方法区出现OOM的异常,在JDK 8中将很难出现(除非本地内存不足)。HotSpot提供了一些参数作为元空间的防御措施:

  1. -XX:MaxMetaspaceSize:设置元空间的最大值,默认为-1即之受限于本地内存。
  2. -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集器进行类型写在,同时GC会对该值进行调整(若释放大量的空间,则降低该值;若释放了很少的空间,则在不超过-XX:MetaspaceSize(已设置的基础上)的基础上提高该值)
  3. -XX:MinMetaspaceFreeRatio:在垃圾收集后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集的频率。
  4. -XX:MaxMetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比。
/**
 * -XX:MetaspaceSize=200M -XX:MaxMetaspaceSize=200M
 */
public class JDK8MetaspaceOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(JDK8MetaspaceOOM.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create();
        }
    }
}
···
Caused by: java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	... 8 more

内存

本机直接内存溢出

直接内存(Direct Memory)的容量大小可以通过-XX:MaxDirectMemorySize参数来指定,默认为Java堆最大值(由-Xmx)。

import sun.misc.Unsafe;
import java.lang.reflect.Field;

/**
 * 使用unsafe分配本机内存
 */
public class DirectMemoryOOM {
    private static final int _5MB = 5 * 1024 * 1024;
    public static void main(String[] args) throws IllegalAccessException {
        Field field = Unsafe.class.getDeclaredFields()[0];
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        while (true) {
            unsafe.allocateMemory(_5MB);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at demo.DirectMemoryOOM.main(DirectMemoryOOM.java:16)

参考文献

《深入理Java虚拟机》周志明

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人生逆旅我亦行人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值