Java Memory Model--学习小结

为什么要学习Java Memory Model?

单从工具的使用上来说,Java内存模型对于实际的代码编写可能不会产生直接的影响。因为Java在很大程度上已经屏蔽了程序员与内存的直接关联。通过Java虚拟机(JVM),程序员可以安全的创建、使用内存数据单元,JVM也会自动的对内存进行垃圾回收处理(Garbage Collector)。但对于一个致力于或者以此为生的工作者来说,不了解JMM,不了解Java内存的分配,就意味着你可能根本就不清楚以下这些:

1. 为什么说早期的Java性能会不如C/C++?

2. 如何提高Java的性能?

3. Java的内存分配、访问和管理比C++具有什么优势?

4. (3)的优势是如何实现的?

5. Java的动态加载特性如何实现?

6. 有哪些情况会出现Out of Memory(OOM)?

7. 出现OOM该如何确定错误原因?

……

如果只是漫无目的的因为使用而使用一门语言,那么其实Python,C,C++,C#,.Net,Perl……任何一门语言都可以满足你的需求,既然已经致力于此,又为何不开始一段新的旅程呢?

自勉并以此作为共勉。JMM,可以作为学习Java的第一步……


字典

缩写全称
JMMJava Memory Model
JVMJava Virtual Mechine
OMMOutOfMemory
PCProgram counter
GCGarbage Collector

1. Java Virtual Memory对运行时内存区域的划分

JVM将其管理的运行时内存区域大致划分为两个区域:1)线程隔离数据区;2)线程共享数据区。根据《Java虚拟机规范规定》,可以将其划分为这么几个部分:

下面就运行时数据区模型的各个组成部分进行介绍。


2. 线程隔离数据区--程序计数器(Program Counter Register)

程序计数器作为当前线程执行字节码的“行号指示器”,其在内存区域的分配区域固定,且占用空间极小。程序计数器能够帮助字节码解释器选取下一条需要执行的字节码指令。在多线程切换时,也需要依赖程序计数器进行线程恢复工作。因此,为了避免各个线程之间的相互影响,程序计数器必须作为“线程私有”。PC没有指定Out Of Memory的任何情况(如果有,那可能是因为平台字节码指令地址超过PC的指定宽度,不过这几乎是不可能的)


3. 线程隔离数据区--虚拟机栈(Java Virtual Mechine Stacks)

虚拟机栈、本地方法栈和方法区,是我在学习过程中碰到容易混淆的几个概念点。先从虚拟机栈开始说起。

虚拟机栈是Java方法执行的内存模型。每个Java线程都拥有一个私有的Java虚拟机栈,随着线程开始而创建,结束而销毁。Java虚拟机栈中存储的是栈帧(Stack Frame)。每个方法被执行的时候都会同时在Java虚拟机栈中创建一个栈帧,在栈帧内存储了方法的局部变量,方法返回值,动态链接和分派的异常。每一个方法从调用、执行完成的过程就是Java虚拟机栈中的栈帧从入栈到出栈的过程。

Java虚拟机栈中的每个栈帧自内存分配完成后,可以认为其栈帧大小是固定不变的。这是因为,局部变量表所需的内存空间在编译期间可知,并且在编译期间分配完成。在方法运行期间不会改变局部变量表的大小。

局部变量表包括:1)基本数据类型(boolean, byte, char, short, int, float, long, double);2)对象引用;3)returnAddress类型。

在Java虚拟机规范中,规定的了Java虚拟机栈可以固定大小或者动态扩展其虚拟机栈大小,并且定义了两种异常情况:

1)当虚拟机栈的大小超过限定大小,JVM抛出StackOverflowError。

2)若虚拟机栈可动态扩容,但已无法为其继续动态扩容。或者无法为一个新的Java线程新建一个虚拟机栈,则抛出OutOfMemoryError。

下面是一段示例程序,创建了一个可能的虚拟机栈内存溢出的情况:

/**
 * <h5>StackOverflowError</h5>
 * 
 * <pre>
 * 当线程请求的虚拟机栈的大小超过限定大小,将抛出<code>java.lang.StackOverflowError</code>错误。
 * </pre>
 * 
 * @author yongqing_wang
 */
public class StackOverflowErrorTest {

    public static void main(String[] args) {
        StackOverflowErrorTest sofe = new StackOverflowErrorTest();
        sofe.recurse();
    }

    public void recurse() {
        recurse();
    }
}

Exception in thread "main" java.lang.StackOverflowError
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)
	at jvm.jmm.overflow.StackOverflowErrorTest.recurse(StackOverflowErrorTest.java:20)

4. 线程隔离数据区--本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈的功能几乎完全一致,其区别是执行的方法不同。Java虚拟机栈为执行Java方法(字节码)服务,而本地方法栈为执行Native方法服务。对于本地方法栈,JVM规范没有对其做任何规定,因此也无法在《JVM虚拟机规范》中找到,其随着具体虚拟机的实现不同而不同。也可以将本地方法栈与虚拟机栈合并。同样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError。其发生情况与虚拟机栈类似。


5. 线程共享数据区--Java 堆(Heap)

Java堆是Java中最重要的一块,其对于Java的运行性能影响也最为重要。Java堆为线程共享数据区域,在JVM启动时创建,其唯一目的就是存放Java对象实例,在《JVM虚拟机规范》中规定,所有的对象实例和数组都在这里分配内存。(现在这已经不是绝对,但在此不做讨论)

Java堆的管理直接影响内存利用率,访问效率等影响Java运行效率的直接参数。因此,Java堆由自动存储管理系统管理(auto storage management system)管理(garbage collector就是其一种实现的名称)其分配、回收和整理。

规范中规定了Java堆可固定大小,也可以由程序员or用户自己分配初始化大小,也可以对堆进行动态扩容,其异常情况:

1)当计算需要更多堆空间时,堆无法为其分配,JVM抛出OOM异常。

下面是一段示例程序,演示了Java堆异常的可能情况:

public class HeapOOMTest {

    public static void main(String[] args) {
        List<String> huge = new ArrayList<String>();

        while (true) {
            String tmp = new String("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
            huge.add(tmp);
        }
    }
}


Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:2760)
	at java.util.Arrays.copyOf(Arrays.java:2734)
	at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
	at java.util.ArrayList.add(ArrayList.java:351)
	at jvm.jmm.overflow.HeapOOMTest.main(HeapOOMTest.java:13)


6. 线程共享数据区--方法区(Method Area)

(方法区对于我而言比较陌生,其原因是对Java的学习还不够深入,在这里只做整理及记录工作。)

方法区是Java堆区的一个逻辑组成部分,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,可以不受GC的管理。(在《JVM虚拟机规范》中是这么描述方法区的:The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in a UNIX process.It stores per-class structures such as the runtime constant pool, field and method data, and the code for methods and constructors, including the special methods(§3.9) used in class and instance initialization and interface type initialization. )

规范中规定了方法区可固定大小,也可以由程序员or用户自己分配初始化大小,也可以对方法区进行动态扩容,其异常情况:

1)当方法区内存不足以满足分配请求,JVM抛出OOM异常。


6.1 运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,用于存放编译期生成的各种数字文字量(numeric literals)和符号引用。运行时常量池提供类似于符号表作用,只是运行时常量池的数据类型远广泛于传统的符号表。运行时常量池在类(class)或者接口(interface)被JVM创建时构建。

我们可以通过javap -p -verbose来查看一个类的运行时常量池:

Compiled from "RuntimeConstantPoolOverflowErrorTest.java"
public class jvm.jmm.overflow.RuntimeConstantPoolOverflowErrorTest extends java.lang.Object
  SourceFile: "RuntimeConstantPoolOverflowErrorTest.java"
  minor version: 0
  major version: 50
  Constant pool:
const #1 = Method	#8.#30;	//  java/lang/Object."<init>":()V
const #2 = class	#31;	//  java/util/ArrayList
const #3 = Method	#2.#30;	//  java/util/ArrayList."<init>":()V
const #4 = Method	#32.#33;	//  java/lang/String.valueOf:(I)Ljava/lang/String;
const #5 = Method	#32.#34;	//  java/lang/String.intern:()Ljava/lang/String;
const #6 = InterfaceMethod	#35.#36;	//  java/util/List.add:(Ljava/lang/Object;)Z
const #7 = class	#37;	//  jvm/jmm/overflow/RuntimeConstantPoolOverflowErrorTest
const #8 = class	#38;	//  java/lang/Object
const #9 = Asciz	<init>;
const #10 = Asciz	()V;
const #11 = Asciz	Code;
const #12 = Asciz	LineNumberTable;
const #13 = Asciz	LocalVariableTable;
const #14 = Asciz	this;
const #15 = Asciz	Ljvm/jmm/overflow/RuntimeConstantPoolOverflowErrorTest;;
const #16 = Asciz	main;
const #17 = Asciz	([Ljava/lang/String;)V;
const #18 = Asciz	args;
const #19 = Asciz	[Ljava/lang/String;;
const #20 = Asciz	list;
const #21 = Asciz	Ljava/util/List;;
const #22 = Asciz	i;
const #23 = Asciz	I;
const #24 = Asciz	LocalVariableTypeTable;
const #25 = Asciz	Ljava/util/List<Ljava/lang/String;>;;
const #26 = Asciz	StackMapTable;
const #27 = class	#39;	//  java/util/List
const #28 = Asciz	SourceFile;
const #29 = Asciz	RuntimeConstantPoolOverflowErrorTest.java;
const #30 = NameAndType	#9:#10;//  "<init>":()V
const #31 = Asciz	java/util/ArrayList;
const #32 = class	#40;	//  java/lang/String
const #33 = NameAndType	#41:#42;//  valueOf:(I)Ljava/lang/String;
const #34 = NameAndType	#43:#44;//  intern:()Ljava/lang/String;
const #35 = class	#39;	//  java/util/List
const #36 = NameAndType	#45:#46;//  add:(Ljava/lang/Object;)Z
const #37 = Asciz	jvm/jmm/overflow/RuntimeConstantPoolOverflowErrorTest;
const #38 = Asciz	java/lang/Object;
const #39 = Asciz	java/util/List;
const #40 = Asciz	java/lang/String;
const #41 = Asciz	valueOf;
const #42 = Asciz	(I)Ljava/lang/String;;
const #43 = Asciz	intern;
const #44 = Asciz	()Ljava/lang/String;;
const #45 = Asciz	add;
const #46 = Asciz	(Ljava/lang/Object;)Z;

规范中规定了运行时常量池的异常情况:

1)当创建一个类或者接口时,运行时常量池需要的内存空间不能够在方法区分配,JVM抛出OOM异常。

下面是一段示例程序,模拟了运行时常量池的溢出:

/**
 * <h5>Runtime Constant Pool overflow.</h5>
 * 
 * <pre>
 * This is because String.intern() returns a constant string using
 * native method.
 * </pre>
 * 
 * @author yongqing_wang
 */
public class RuntimeConstantPoolOverflowErrorTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();

        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at jvm.jmm.overflow.RuntimeConstantPoolOverflowErrorTest.main(RuntimeConstantPoolOverflowErrorTest.java:24)

7. 直接内存

直接内存并不是JVM运行时数据区的一部分,也没有在《JVM虚拟机规范》中进行定义。它可以是作为一种堆外缓存机制存在,在某些场景下可以显著提高性能。

但由于这部分直接内存的分配不会受到Java堆大小的限制,以此在设置JVM参数时需要考虑其存在,防止内存实际分配区域超过实际内存区域而导致的OOM异常。


参考文献:

[1] 《深入理解Java虚拟机》 http://book.douban.com/subject/6522893/

[2] 《Java虚拟机规范》  http://java.sun.com/docs/books/jvms/second_edition/html/Overview.doc.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值