1 java虚拟机内存划分模型
JVM在执行Java程序时,会把它所管理的内存划分为若干个不同的数据区域。JVM与形式数据区域如下:
1.1 程序计数器
程序计数器(program counter register)是一块儿较小的内存空间,它记录的是当前线程所执行的字节码的地址。Java每个线程都有一个独立的程序计数器,此区域内存为线程私有,记录本线程执行的位置,各个线程的程序计数器互不影响。
注意:当线程正在执行的是native方法时,这个计数器值为空(undefined)。
是否抛异常:此内存区域是唯一一个在JVM规范中没有规定任何outOfMemoryError情况的区域。
1.2 Java虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
虚拟机栈为虚拟机执行Java方法(就是字节码)服务。
注意:局部变量表存放编译期可知的各种数据类型:基本数据类型(boolean、byte…long、double)、对象引用(reference类型)和returnAddress类型。其实定义一个方法所涉及的变量无非就是这些类型。
是否抛异常:线程请求的栈深度超虚拟机所限则抛*Error异常;虚拟机栈进行动态扩展时,无法申请到足够内存则抛OutOfMemoryError。
1.3 本地方法栈
本地方法栈(native method stacks)是为虚拟机使用到的native方法服务。虚拟机规范对本地方法栈中的方法使用的语言、使用方式及数据结构没有明确规定,由具体虚拟机*实现。
是否抛异常:与虚拟机栈一样的情况会抛*Error、OutOfMemoryError。
1.4 Java堆
Java 堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。
Java堆内存区域的唯一用途就是存放对象实例,几乎所有的对象实例都在这里分配内存。注意,随着JIT编译器发展和逃逸分析技术的成熟,对象在堆上分配并不是绝对的了。
垃圾回收器回收的主要是类实例,因此Java堆是垃圾收集器管理的主要区域。
Java堆在物理上可以是不连续的内存空间。
当前主流虚拟机的堆大小都是可扩展的。
是否抛异常:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
1.5 方法区
方法区(method area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。用途跟Java堆有明显区别。
常量是存放在运行时常量池(rumtime constant pool)中的,运行时常量池是方法区的一部分。
该区域内存空间可不连续,内存大小可选择固定或者扩展,可以对此区域不实现垃圾回收,由具体虚拟机决定。
是否抛异常:当方法区内存无法满足需要时将抛出OutOfMemoryError异常。
1.6 直接内存
直接内存(Direct Memory)不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。但是像Java的NIO类常会使用这部分内存来被Java堆中的对象引用以提高性能。
直接内存受计算机本身总内存大小及处理寻址空间的限制。
是否会抛异常:在为虚拟机配置的内存参数超过物理内存限制时,将导致在动态扩展时出现OutOfMemoryError异常。
1.7 关于对象访问的一个问题
问题:在一个方法中,常见的创建对象的方式形如:Object obj = new Object(); 这句代码反映到内存模型中是怎样的呢?
首先,new Object() 实例将存储在Java堆内存的实例池中,这个实例的类型数据将存储在方法区;obj变量将存储在Java虚拟机栈的本地变量表中,作为reference类型。
不同虚拟机的对象访问方式主要有两种:句柄访问、直接指针访问。
若句柄访问,则obj中存储的是对象的句柄地址,这个句柄包含有指向new Object()创建的对象实例数据的指针和指向对象类型数据的指针。模式如下:
若使用直接指针访问,则obj变量存储的对象的地址。模式如下:
2 垃圾收集与内存分配
2.1垃圾收集器收集的是什么?
Java堆是垃圾收集器管理的主要区域,而虚拟机规范中中明确规定:所有的类实例和数组都要在Java堆中分配。因此垃圾收集器收集的主要是类实例和数组(Java中数组也是一组类实例)。
2.2 对象什么时候被收集?
对象不会被通过任何途径使用时就需要回收。判断方法有两个,引用计数法、可达性分析算法。
引用计数算法(Reference Counting)的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软公司的COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言和在游戏脚本领域被广泛应用的Squirrel中都使用了引用计数算法进行内存管理。但是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。
2.3垃圾收集器的收集算法是什么?
几种典型收集算法:
(1)标记-清除算法,缺点是会产生内存碎片。
(2)复制算法,缺点是需要预留一部分内存空间用于保存从另一块内存区域复制而来的存活对象,减少了可使用内存。
(3)标记-整理算法,特别使用于老年代对象的垃圾收集。
(4)分代收集算法,即新生代采用复制算法而老年代采用标记-整理算法,这是目前商用虚拟机使用的主流垃圾收集算法。