一、JVM 结构简图
- 线程私有:虚拟机栈、程序计数器、本地方法栈
- 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)
二、运行时数据区域
1.堆
堆(Heap)在虚拟机启动时建立,它是Java虚拟机管理的内存中最大的一块,用来存放几乎所有java对象的实例,被所有线程共享。
默认 新生代(Eden : S1 : S2 = 8 : 1 : 1): 老年代 = 1 : 3
1.1 新生代(Young)
新生代对象朝生夕死(一次垃圾收集一般可以回收70% ~ 95% 的空间),存活率很低,一般都存放再新生代(新生大对象存放在老年代)。新生代又可细分为Eden空间、From Survivor(S1)空间、To Survivor(S2)空间,默认比例为8:1:1。
1.2 老年代(Tenured/Old)
在新生代中经历了多次(虚拟机默认阀值15)GC后仍然可以存活下来的对象会进入老年代中。老年代中的对象一般生命周期都较长,存活率比较高(短命大对象),在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
1.3 永久代(Perm)
永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集(一般也不会进行垃圾回收),永久代不属于堆空间,它在JDK 1.8之后被元空间取代。
2.方法区
方法区(Method Area/Non-Heap)用来存放由类加载子系统从文件系统或者网络中加载class信息;除了存放类信息外方法区中还存放常量池(Constant Pool)运行时常量池(Runtime Constant Pool),被所有线程共享。
Java7前,方法区中存储类信息、常量、静态变量、jit编译时代码。
Java7前,常量池是存放在方法区(永久代)中的。
Java7后,常量池是存放到了堆中。
Java8后,元空间取代了永久代。 运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。
3.虚拟机栈
每个线程在被创建的时都会创建一个虚拟机栈(Java Virtual Machine Stack),其内部由一个个的栈帧(Stack Frame)组成,因此虚拟机栈是线程私有的(无需考虑垃圾回收,与线程同生共死);每运行一个方法就创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法返回值等。每一个方法开始运行到运行完成的过程就是一次入栈和出栈的过程。
栈分为固定大小的栈与可扩容的栈,固定大小的栈在空间不足时会报StackOverflowError,可扩容的栈再空间不足同时无法扩容时会报OutOfMemoryError。
堆、栈、方法区的关系
指针定位
句柄定位
地址定位:速度快(减少定位次数),对象地址变更后需要调整Reference。【Hotspot使用】
句柄定位:对象地址信息变动只需调整句柄池中的对象地址,Reference无需调整。
GC标记整理,复制算法都会导致对象内存地址变更。
4.程序计数器
程序计数器(Program Counter Register)用于保存当前线程执行的内存地址,可以看作当前线程执行的字节码的行号指示器。由于JVM程序是多线程执行的,线程间需要轮流切换,因此为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方;程序计数器也是线程私有的(无需考虑垃圾回收,与线程同生共死)。当线程正在执行的是 Java 方法时,程序计数器记录的是 JVM 字节码指令地址,当执行 native 方法时,程序计数器记录的是 undefined。
5.本地方法栈
本地方法栈(Native Method Stack)与虚拟机栈的作用差不多,是为JVM使用到的native方法服务的(本地方法是使用C语言实现的)。