JVM包含两个子系统和两个组件,分别为
- Class loader(类装载子系统) :根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的方法区中。
- Execution engine(执行引擎子系统):执行class的指令。
- Runtime data area(运行时数据区组件):即我们常说的JVM的内存。
- Native Interface(本地接口组件):与native lib交互,是其它编程语言交互的接口。
2.2 运行时数据区域
JVM所管理的内存包括一下几个运行时数据区域
线程私有
- 程序计数器
- JVM栈
- 本地方法栈
线程共享
- 方法区
- Java堆
2.2.1 程序计数器
由于JVM堆多线程是通过线程轮流切换、分配处理器执行时间堆方式来实现的,因此为了线程切换后能恢复到正确堆执行位置,每条线程都需要有一个独立堆程序计数器。如果线程正在执行的是一个Java方法,计数器记录的是正在执行堆虚拟机字节码指令堆地址;如果正在执行的是一个本地方法,计数器的值则为空。
2.2.2 JVM栈
Java方法执行的线程内存模型:每个方法被执行的时候,JVM会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应一个栈帧在JVM栈中从入栈到出栈的过程。
该内存区域的两类异常状况:
- StackOverFlowError异常:线程请求的栈大于JVM所允许的深度;
- OutOfMemoryError异常:如果JVM栈容量可以动态扩展,当栈扩展时无法申请到足够的内存。
2.2.3 本地方法栈
和JVM栈发挥的作用类似,区别是JVM栈为JVM执行Java方法服务,本地方法栈为JVM使用到的本地方法服务。有的JVM将JVM栈和本地方法栈合二为一(如HotSpot虚拟机)。同样会抛出两类异常。
2.2.4 Java堆
只存放对象实例,不存放基本类型和对象引用。是垃圾收集器管理的内存区域,也被成为GC堆 。所有线程共享的Java堆里可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer TLAB),以提升对象分配时的效率和更好地回收内存,同时可以解决创建对象时的线程安全问题。
Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
Java堆既可以被实现成固定大小,也可以是可扩展的(当前主流)。当无法再扩展时,会抛出OutOfMemoryError异常。
2.2.5 方法区
用于存储已被JVM加载的类型信息、常量、静态变量、即时编译后的代码缓存等数据。相对而言,该区的垃圾回收行为比较少出现(但未完全回收时会导致内存泄漏)。该区域的内存回收目标主要针对常量池的回收和对类型的卸载。
1.8以后,由本地内存的元空间实现。
2.3 JVM对象
2.3.1 JVM中对象的创建
- 当JVM遇到一条字节码new指令时,首先将检查这个指令当参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 在类加载检查通过后,接下来JVM将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。
Java堆是否规整由所采用的垃圾收集器是否带有空间压缩整理(compact)的能力决定。带有时系统采用的分配算法是指针碰撞。
指针碰撞分配方式:当Java堆中的内存绝对规整时,使用过的内存和空闲的内存被一个作为分界点指示器的指针分隔,分配内存即把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
空闲列表分配方式:当Java堆中的内存不是规整的时,JVM必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- 接下来,JVM还要对对象进行必要的设置。
2.3.2 对象的访问定位
主流的访问方式主要有使用句柄和直接指针两种:
- 使用句柄访问:
- 使用直接指针访问:
使用直接指针的最大好处是速度更快,而对象访问在Java中非常频繁。