下图展示了JVM的主要结构:
可以看出,JVM主要由以下几部分组成: - 类加载器子系统 - 运行时数据区(内存空间) - 执行引擎 - 本地方法接口
运行时数据区又分为: 1. 程序计数器 2. Java栈 3. 本地方法栈 4. 方法区 5. 堆
其中 方法区 和 堆 是所有Java线程共享的,而Java栈、本地方法栈、PC寄存器则由每个线程私有。
1. 程序计数器(Program Counter Register)
程序计数器可以看做是当前线程所执行的字节码的行号指示器。 字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令。
线程私有: - 为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
OOM: - 不会出现OOM。
2. Java栈(Java Stack)
Java栈描述的是Java方法执行的内存模型。 Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。 Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。
线程私有: - 前面已经提到Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区。
OOM: - 如果线程请求的栈深度大于JVM所允许的深度,则抛出StackOverflowError。 - 如果VM可以动态扩展,但是扩展是无法申请到足够的内存,则抛出OutOfMemoryError。 - 可以通过减少-Xss,同时递归调用某个方法,模拟StackOverflowError
它分为三部分:局部变量区、操作数栈、帧数据区。
1、局部变量区
局部变量区是以字长为单位的数组,在这里,byte、short、char类型会被转换成int类型存储,除了long和 double类型占两个字长以外,其余类型都只占用一个字长。特别地,boolean类型在编译时会被转换成int或byte类型,boolean数组会被当做byte类型数组来处理。局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。 局部变量区包含了 方法参数 和 局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。
2、操作数栈
操作数栈也是以字长为单位的数组,但是正如其名,它只能进行入栈出栈的基本操作。在进行计算时,操作数被弹出栈,计算完毕后再入栈。
3、帧数据区
帧数据区的任务主要有: 记录指向类的常量池的指针,以便于解析。 帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操作数栈中。 记录异常表,发生异常时将控制权交由对应异常的catch子句,如果没有找到对应的catch子句,会恢复调用方法的栈帧并重新抛出异常。
局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依此分配栈帧内存,压入Java栈。
3. 本地方法栈(Native Method Stack)
本地方法栈类似于Java栈,主要存储了本地方法调用的状态。 在Sun JDK中,本地方法栈和Java栈是同一个。
线程私有: - 同上
OOM: - 同上
4. 方法区(Method Area)
存储 类型信息 和 类的静态变量。 在Sun JDK中,方法区对应了永久代(Permanent Generation),默认最小值为16MB,最大值为64MB。
方法区中对于每个类存储了以下数据: - 类及其父类的全限定名(java.lang.Object没有父类) - 类的类型(Class or Interface) - 访问修饰符(public, abstract, final) - 实现的接口的全限定名的列表 - 常量池 - 字段信息 - 方法信息 - 静态变量 - ClassLoader引用 - Class引用
可见类的所有信息都存储在方法区中。
线程共享: - 由于方法区是所有线程共享的,所以必须保证线程安全,举例来说,如果两个类同时要加载一个尚未被加载的类,那么一个类会请求它的ClassLoader去加载需要的类,另一个类只能等待而不会重复加载。
OOM: - 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。 ——OSGi这种频繁自定义ClassLoader的场景,需要虚拟机剧本类卸载功能,以保证永久代不会溢出。 - 通过限制永久代大小-XX:PermSize, -XX:MaxPermSize;同时大量添加常量池;或借助CGLib生成大量动态类,可以模拟OutOfMemoryError。 ——注:运行时添加常量池可用list.add(String.valueOf(i++).intern()) //Jdk7以下
5. 堆(Heap)
堆用于存储 对象实例 以及 数组。 堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。
线程共享: - 堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。 - 此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。
OOM: - Java堆是垃圾收集器管理的主要区域。 - 如果堆中没有内存完成实例分配,并且堆也无法再扩展时,则抛出OutOfMemoryError。 - 可以通过减少-Xms, -Xmx;同时创建无数对象来模拟OutOfMemoryError。 - 同时-XX:+HeapDumpOnOutOfMemoryError,可以dump出当前的内存堆转储快照,以便分析。
在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为新生代、旧生代。分代方式大大改善了垃圾收集的效率。
1、新生代(New Generation)
大多数情况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时的对象复制(Minor GC的过程在此不详细讨论)。 JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配,我们知道在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。
2、旧生代(Old Generation/Tenuring Generation)
在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。