Java虚拟机在执行Java程序的过程会把它所管理的内存划分为若干不同区域,各自有各自的用途,创建和销毁的时间.包括以下几个运行时数据区域.
程序计数器可以看作当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.
每一个线程都有一个独立的程序计数器,防止线程轮流切换后不能恢复到正确的执行位置.
如果当前线程执行的是Java方法,那么这个计数器记录的就是正在执行的虚拟机字节码指令的地址.如果执行的是Native方法(一个Native Method就是一个Java调用非Java代码的接口),则计数器的值为空(Undefined).
这个区域中没有规定任何的OutOfMemoryErroe(内存溢出)情况.
Java虚拟机栈也是线程私有的,生命周期与线程相同.描述的是**Java方法执行的内存模型**.
每一个Java方法在执行时都会创建一个栈帧(Stack Frame),存储局部变量表,操作数栈,动态链接,方法出口等信息.一个方法从开始执行到执行完成,就对应着这个方法的栈帧在Java虚拟机栈中入栈到出栈的过程.
局部变量表中存放了编译期间可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(可能是直接地址的引用指针,也可能是一个句柄)和**returnAddress类型**(指向了一条字节码指令的地址,returnAddress中保存的是return后要执行的字节码的指令地址,方法执行完之后重新定位).
64位长度的long和double类型的数据会占用2个局部变量空间,其他的数据类型只占据一个.
局部变量表所需的内存空间在**编译期间就完成了分配,是完全确定的,方法的运行不会改变局部变量表的大小.**
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常.如果动态扩展虚拟机栈时无法申请到足够的内存,会抛出OutOfMemoryError异常.
与Java虚拟机站十分相似,区别是虚拟机栈执行Java方法,而**本地方法栈则为虚拟机使用到的Native方法服务**.
异常方面都一样.
对绝大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块.被所有线程共享,在虚拟机启动时创建.
此区域的唯一目的就是存放对象实力,几乎所有的对象实力都在这里分配内存.
Java堆是垃圾收集器管理的主要区域.
从内存回收角度看,现在收集器基本都采用分代收集算法,所以Java堆还可以细分为**新生代和老生代**.
从内存分配的角度看,线程共享的Java堆中**可能划分出多个线程私有的分配缓冲区**.
如果在堆中内有内存完成实例分配,并且堆也无法在拓展时,将会抛出OutOfMemoryError异常.
方法区与Java堆一样,被各个线程共享,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常.
永久代.
运行时常量池是方法区的一部分.class文件中有一项信息是常量池,用于存放编译器生成的各种**字面量和符号引用**,这部分内容将在类加载后进入方法区的运行时常量池存放.
运行时常量池相对于Class文件常量池的一个重要特征是具备动态特性,可以在运行期间加入新的常量.
直接内存并不是虚拟机运行时数据区的一部分.
JDK1.4中新加入了NIO类,引入了基于通道(Channel)和缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作.这样做可以提高性能,避免在Java堆和Native堆中来回复制数据.
如果各个区域内存综合大于物理内存限制,动态拓展时会出现OutOfMemoryError异常.
当虚拟机遇到一条new指令时,首先将去将**检查这个指令的参数是否能在常量池中定位到一个类(也就是根据类的标识找到new的类),并检查这个类是否被加载,解析和初始化过,如果没有进行相应的类加载过程**.
类加载检查通过后,虚拟机为新生对象分配内存.对象所需内存的大小在类加载后就已经完全确定.
如何从Java堆中划分内存分配给新生对象,有两种方式:
指针碰撞:Java堆中的内存是绝对规整的,用过的内存在一边,空闲的在另一边,中间放置一个指针作为分界.这样分配内存时只需要移动分界指针响应的距离.空闲列表:维护一个列表,列表中记录空闲的内存空间,分配内存时从记录的空闲内存中取出足够大的内存划分给对象实例,并更新这个列表. 采用那种分配方式取决与Java堆是否规整,而Java堆是否规整又取决于垃圾回收机制是否有压缩整理功能.
并发情况下分配内存不是线程安全的,有两种解决方案:
每一次分配内存时都使用线程锁同步处理.给每一个线程在Java堆中分配一小块内存,成为本地线程分配缓冲.给该线程内部分配内存使用.当这个缓冲使用完并分配新的初始化内存时加锁从Java堆中进行分配. 内存分配完成后,虚拟机**将分配到的内存空间都初始化为零值**.
接下来,虚拟机堆对象进行必要的设置,例如是那个类的实例,符合找到元数据(元数据是指用来描述数据的数据,更通俗一点,就是描述代码间关系,或者代码与其他资源(例如数据库表)之间内在联系的数据)信息,对象的hash码,对象的GC分代信息
然后,new一个对象在虚拟机中的过程就算是完成了.
Java程序**开始执行init方法**(构造),这样才算完成了对象的创建.
在HotSpot虚拟机中,对象在内存中存储的布局分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding).
对象头包含两部分信息.第一部分用于存储对象自身的运行时数据,如hash码等.这部分长度在32位和64位虚拟机中分别为32bit和64bit.但是往往信息的大小都超出了32位,64位Bitmap能记录的限度,所以会根据对象的状态复用自己的存储空间.另一部分是类型指针,就是对象指向它的类元数据,虚拟机通过这个指针确定这个对象是那个类的实例.另外,如果对象是Java数组,对象头中还需要记录数组长度.
实力数据部分是对象真正存储的有效信息,也就是程序代码中定义的各种类型的字段内容.
对齐填充并不是必然存在的,仅仅起占位符的作用.**自动内存管理系统要求对象起始地址必须为8字节的整数倍,所以内一个对象的大小都必须为8字节的整数倍.**当对象实例部分没有对齐时,就通过对齐填充来补全.
定位堆中对象的具体为止,并访问的方式主流的有使用句柄和直接指针两种.
如果使用句柄访问,Java堆中会划分出一块内存来作为句柄池,而句柄中包含了对象实例数据与类型数据各组的具体地址.这种方式的好处是在对象被移动(垃圾收集时经常会移动对象)时,reference不需要更改,只需要更改句柄中的示例数据指针就可以了.
如果使用直接指针访问,那么Java堆对象的布局就要考虑如何放置访问类型数据的相关信息.这种方式的好处是访问速度更快,节省时间开销.
设置虚拟机启动参数:
使用控制台命令执行程序,将参数写在Java命令后就可以使用eclipse IDE在Debug/Run中设置OutOfMemoryError
public class Test { private static class OOMObject{} public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while (true){ //无限的创建对象会耗尽Java堆中的内存,导致内存溢出 list.add(new OOMObject()); } } }HotSpot虚拟机中并不区分虚拟机栈和本地方法栈.
线程请求的栈深度大于虚拟机允许的最大深度:StackOverflowError
public class Test { private int stackLength = 1; private void stackLeak(){ stackLength++; //无限的进行方法的调用(这里大多数由于递归引起),会耗尽虚拟机栈中的内存空间 stackLeak(); } public static void main(String[] args) { new Test().stackLeak(); } }虚拟机在扩展栈时无法申请到足够的内存空间:OutOfMemoryError
public class Test { private void dontStop(){ while (true){ //do nothing } } public void stackLeakByThread(){ while (true){ Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); //由于每一个线程都拥有自己的虚拟机栈 //无限的创建线程就会一只创建虚拟机栈,直到计算机的物理内存被耗尽 //这时如果在新创建一个新的线程,为其创建虚拟机栈时就会创建不成功,也就是申请不到自购的内存空间 thread.start(); } } public static void main(String[] args) { new Test().stackLeakByThread(); } }OutOfMemoryError
public class Test { public static void main(String[] args) { // 使用List保持常量池引用,防止GC List<String> list = new ArrayList<>(); int i = 0; while (true){ // String.intern是一个Native方法,作用是: // 如果字符串常量池中已经包含一个等于此String对象的字符串,则返回池中的这个串 // 如果不存在,将此字符创添加到字符创常量池,并返回引用 list.add(String.valueOf(i++).intern()); } } }