众所周知JAVA和C++有一个很大的区别就是JAVA的内存分配及垃圾的回收都是交给JVM实现的,程序员没有权利去进行操作。这样做确实很给程序员带来很大的方便,但是一旦出现诸如内存泄露等情况处理起来有比较大的困难。相反C++程序员对内存的管理享有最高权限,虽然在编写程序时比较繁琐,但是出现问题后处理起来比较容易。有一句很经典的话说JAVA和C++之间有一堵由动态分配内存和垃圾收集技术围成的“围墙”,墙外的人想进去,墙里面的人想出来。
想要翻过虚拟机内存管理这道墙,让我们以后遇到如内存泄露等情况处理起来能得心应手,那么我们就应该首先了解JAVA虚拟机中内存的各个区域。总体来讲JAVA内存分配分为线程共享与线程独享。其中方法区和堆区是线性共享,而程序计数器、JAVA虚拟机栈、本地方法栈属于线程隔离的,他们随着线程的创建而分配,随着线程的消亡而消亡。这些去可以用一个图简单表示:
在讨论每个分区之前我们先来看看线程私有的内存的概念。我们直到在多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个时刻,处理器只能处理一个线程中的指令,因此在保证每次线程恢复时能执行到正确的位置,我们应该记录下一些线程的信息,这样各条线程互不影响,独立存储。记录这些数据的内存区域叫线程独享区域。
下面我们就每个区进行一下详细的讨论:
1、程序计数器:
简单讲程序计数器就是一个字节码执行的行号指示器,在虚拟机的概念模型里面字节码解释器就是通过改变程序计数器的值来确定下一条需要执行的字节码指令。在JAVA的分支,循环,跳转,异常处理以及线程恢复等基础功能都需要程序计数器的支持。因此程序的每一个线程都应该有一个程序技术器,当该线程执行的是JAVA方法时,程序计数器总是指向将要执行的字节码的地址,但是当执行一个native方法时,程序计数器的值为undefined。程序计数器也是唯一一个在JAVA虚拟机规范中没有规定任何outOfMemorryError情况的区域。
2、JAVA虚拟机栈:
一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作栈、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。
每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。
3、本地方法栈:
本地方法栈与JAVA虚拟机栈的作用其实差不错,最大的不同是虚拟机栈是为虚拟机执行JAVA方法服务而本地方法栈是为Native方法服务的,甚至在有些虚拟机中将2者合二为一。
4、JAVA堆:
堆区使我们接触最多也是所有与内存区域中最大的一块。堆区唯一的作用是为对象分配内存及存放内存实列。因此这个区也是垃圾收集器管理的重要区域。因为我们的程序在运行时总是不断的伴随着对象的产生于消亡。 一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主 流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
5、方法区:
方法区主要用于存储已经被虚拟机加载的类信息,常亮,静态变量,及时编译器编译后的代码。虽然在虚拟机规范中把方法区描述为堆的一部分,但是我们还在将他理解为独立的一部分,这样便于内存的管理。
6、运行时常量池:
运行时常量池是方法区的一部分,我们知道class文件在被编译后除了有类的版本,字段,方法,接口等描述信息以外,还有一些各种字面量和符号引用,那么这部分内容就会今天运行时常量池。运行时常量池还有一个重要特征是动态性即我们可以在运行时将常量放入池中。平时运用的比较多的是String类的intern()方法。
7、直接内存:
事实上直接内存不是虚拟机运行时数据区的一部分,也不是java中虚拟机规范中定义的内存区域。可以这样理解直接内存就是除了JVM以外的内存,比如你的机器是4G内存其中你的JVM占用了1G那么你的直接内存就是3G。