Java程序员一般不需要太关注内存,因为操作内存的权力都交给了Java虚拟机,但是Java程序员必须需要了解JVM是如何使用内存的,否则一旦内存出现泄漏或事溢出的话,就会一筹莫展不知道从哪去入手排查问题。
一、JVM内存模型
JVM在运行时会把它管理的内存划分成若干个不同区域,每个区域有各自不同的用处,以及不同的创建和销毁的时间,有的随着JVM进程启动而存在,而有的需要随着用户线程的启动和结束而创建和销毁,主要内存区域划分如下图示:
主要分成两大类,线程共享的区域和线程独享的区域。
线程共享区域
1.堆
堆内存是JVM管理的最大的内存区域,堆内存被所有线程共享,随着虚拟机的启动而创建,存放的全部是对象实例,几乎所有的对象实例都是在堆内存中被分配内存。随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术会导致对象不一定会在堆中分配内存了。
堆内存是垃圾回收主要的区域,而主流的垃圾回收都是采用的分代收集算法,所以堆内存也常常会被分为新生代和老年代,新生代又可以被分成Eden区、From Survivor区和To Survivor区。堆内存虽然可以划分成很多区,但仅仅是逻辑上的区分,实际存储的都是对象实例。而且堆内存可以在物理内存空间上不连续,只需要逻辑上连续即可。一般JVM优化主要也是针对堆内存来进行优化。而堆内存的扩展主要是通过-Xms和-Xmx参赛来进行配置,当堆内存不足以再创建新对象时就会抛出OutOfMemoryError异常
2.方法区
方法区和堆内存一样也是所有线程共享的,主要存储已经被虚拟机加载的类信息、静态变量、常量、即时编译器编译后的代码等信息。由于方法区存储的一般都是不变的数据,所以垃圾回收很少会在这个区域进行。方法区的垃圾回收主要是针对常量池和对类型的卸载。这个区域如何内存不足也会抛出OutOfMemoryError异常。
线程独享区域
1.虚拟机栈
虚拟机栈属于线程私有,随着线程的创建而创建,随着线程的销毁而销毁。而每执行一个Java方法,都会在虚拟机栈中创建一个栈帧 (Stack Frame),栈帧中又包含局部变量表、操作栈、动态链接和方法出口等信息。每一个Java方法被调用直到方法执行完成的过程,对应着一个栈帧在虚拟机栈中的入栈到出栈道过程。一般有一种说法是Java内存分为堆内存和栈内存,这种是比较笼统的,这里的栈内存实际指的仅仅是虚拟机栈中的一个栈帧的局部变量表区域。局部变量表中存储八种基本数据类型、对象引用(不同的虚拟机可能是指向对象起始地址的引用指针,也可能是指向一个对象的句柄或与对象相关的位置)
虚拟机栈内存区域每执行一个方法,有创建一个栈帧压栈,执行完后出栈,栈的特点是先进后出,后进先出,如果线程请求的栈深度大于虚拟机允许的深度,会抛StackOverFlowError(栈溢出);而如果虚拟机栈内存不足且扩展时也无法申请到足够的内存的话,则会抛OutOfMemoryError(内存溢出)
1.1、局部变量表:存储方法局部变量
1.2、操作数栈:出入栈进行操作数的计算
1.3、动态连接:动态寻址的过程,比如方法定义UserService userService; 那么实际运行的时候需要找到UserService接口具体是由哪个对象实现的
1.4、顾名思义就是方法的出口
下面以简单例子查看虚拟机栈的工作方式:
1 public static Integer num = 10;
2
3 public int add(int i){
4 int j = 5;
5 int k = i+j;
6 j++;
7 k = num + j;
8 return k;
9 }
定义一个简单的add方法,定义一个静态变量,代码编译之后结果如下:
1 public int add(int);
2 descriptor: (I)I
3 flags: ACC_PUBLIC
4 Code:
5 stack=2, locals=4, args_size=2
6 0: iconst_5
7 1: istore_2
8 2: iload_1
9 3: iload_2
10 4: iadd
11 5: istore_3
12 6: iinc 2, 1
13 9: getstatic #2 // Field num:Ljava/lang/Integer;
14 12: invokevirtual #3 // Method java/lang/Integer.intValue:()I
15 15: iload_2
16 16: iadd
17 17: istore_3
18 18: iload_3
19 19: ireturn
20 LineNumberTable:
21 line 7: 0
22 line 8: 2
23 line 9: 6
24 line 10: 9
25 line 11: 18
(具体每一步的含义及局部变量表和操作数栈的变化情况在《JVM探秘6--图解虚拟机栈的局部变量表和操作数栈工作流程》详细图解)
2.程序计数器
程序计数器占有的内存较小,主要存储当前线程执行的字节码的行号,字节码执行器工作时需要根据程序计数器来选取下一条需要执行的字节码指令,由于JVM的多线程数通过线程轮流切换被分配CPU执行的,一个CPU同一时间只能处理一个线程的一个指令,为了在线程切换后能够恢复到正确的指令位置,就需要每个线程都有一个独立的程序计数器,各个线程之间的程序计数器互不影响。如果线程正在执行JAVA方法,则计数器记录正在执行的字节码指令的地址,如果正在执行的事本地方法,计数器值则为空。程序计数器由于存储的值只有字节码地址,没有扩展的需要,所以这个区域是不会出现OutOfMemoryError情况的。
3.本地方法栈
本地方法栈和虚拟机栈类似,不同的是虚拟机栈是为Java方法服务,而本地方法栈是为本地方法服务,而本地方法使用的语言不同的虚拟机是不一样的,也有的虚拟机会将本地方法栈和虚拟机栈合二为一。本地方法栈和虚拟机栈一样也会抛StackOverFlowError和OutOfMemoryError。
除了上面的JVM运行时内存划分,还有一类内存叫做直接内存,这类内存是不受JVM管控的,但是也会出现OutOfMemoryError异常。JDK1.4加入了NIO,引入了通道和缓冲区的I/O方式,可以使用本地方法直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作,可以避免java堆和native堆中来回复制数据而提高性能。直接内存不受JVM控制,但是会受到机器总内存的影响,如果JVM内存分配过大加上其他的内存大于了服务器的总内存,就会导致直接内存无法分配,同样也会出现OutOfMemoryError异常。