Java 与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙里的人想出来。
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不需要在为每new一个对象去做delete\free操作,不容易出现内训泄漏和内存溢出的问题,有虚拟机机管理内存,这一切看起来都很美好,不过也正是Java程序员把 内存的控制权利交给Java虚拟机,一旦出现内存泄漏和内存溢出,如果不知道Java虚拟机是如何使用内存的,那么排查错误将成一件很困难的事情。
1.运行时数据区域
Java虚拟机在运行Java程序时把它管理的内存分成若干个不同的数据区域,这些内存区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟的启动而存在,有的区域随着线程的启动和结束而建立和销毁。
1.1程序计数器
一块较小的内存空间,可以看作当前线程执行字节码的行号指示器。
由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核),都只会执行一条线程中的指令。因此,在线程切换之后能够回复到正确的位置,每一个线程需要一个独立的程序计数器,各个线程之间互不影响,独立存储,我们称这类内存为“线程私有”的内存。
如果线程正在执行Java方法,程序计数器记录的是正在执行的虚拟机字节码的指令的地址;如果正在执行的是Native方法,这个计数器值则为空,此内存区域是Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
1.2 Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的声明周期与线程相同。
Java虚拟机栈描述的是Java方法执行的内存模型:每一个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息,一个方法从调用直至执行完成的过程,就对应着一个栈帧(Stack Frame)在虚拟机栈中入栈和出栈的过程。
在虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟所允许的深度,将抛出StackOverflowError;如果虚拟机栈可以动态扩展,但是扩展时无法申请到足够的内存,就会跑出OutOfMemoryError。
1.3 本地方法栈
本地方法栈和虚拟机方法栈发挥的作用是非常的相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法而服务的,本地方法栈是为虚拟机执行Native方法而服务的。
有的虚拟机(譬如:Sun HotSpot虚拟机)直接将虚拟机栈和本地方法栈合二为一,与虚拟机栈一样,版本方法栈也会跑出StackOverflowError和OutOfMemoryError。
1.4 Java堆(Java Heap)
对于大多数应用来说,Java堆是虚拟机所管理的内存中最大的一块,Java堆是所有线程所共享的内存区域;在Java虚拟机启动时创建。
此内存区域 的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,在一点在Java虚拟规范中描述:所有的对象以及数组实例都要在堆上分配内存,但随着JIT编译技术的发展以及逃逸技术逐步成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也渐渐的变得不是那么绝对。Java是垃圾收集器管理的主要区域,因此很多时候被称为“GC”堆。
如果在堆中没有内存完成实例分配并且堆也无法再扩展时将会抛出OutOfMemoryError。
1.5 方法区
方法区(Method Area),和Java堆一样是线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译之后的代码;虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它有一个名称叫“Non-Heap”(非堆),目的是和Java堆区分开来。
对于习惯在HotSpot虚拟机上开发,部署程序的开发人员来说,很多人更愿意把方法区称作“永久代”,本质上两者并不等价;仅仅是HotSpot虚拟机设计团队选择把GC分代收集扩展到方法区中或者选择使用永久代方法来实现方法区而已,HotSpot虚拟机垃圾收集器就可以像管理对一样管理这一部分内存,不需要再为方法编写内存管理代码。对于其他虚拟机(JRockit,J9等)来说是不存在永久代的,原则上来说,如何实现方法区 是属于虚拟机的实现细节,不收虚拟机规范的约束,但是使用永久代来实现方法区不是一个好主意。
根据Java虚拟机的规范规定,当方法无法满足内存分配的需要时将会抛出OutOfMemoryError。
1.6 运行时常量池
运行时常量池是方法区的一部分,Class文件除了有类的版本,字段,方法,接口等描述信息之外,还有一项信息是常量池;
用于存放编译期生成的字面量和符号引用,这部分内容将在类加载之后进入方法区中的运行时常量池中存放。
Java语言并不要求常量只有编译时才能产生,也就是并非预置到class文件中常量池的内容才能进入到方法区运行时常量池,运行区间也可以把新的常量放到池中,使用得比较多的是String的intern()方法。当常量池无法申请到内存时将会抛出OutOfMemoryError,
1.7直接内存
直接内存并不是虚拟机运行时数据区域的一部分,也不是Java虚拟机规范中定义的内存,但是这部分内存也被频繁使用,而且也可能导致OutOfMemoryError。