JVM三大核心部分:运行时数据区,类加载器和执行引擎。
执行顺序:
- 类加载器:解析 .class文件 转为虚拟机可以识别的二进制机器码
- 执行引擎:解析 字节码文件 使用执行引擎 驱动去加载机器码
- 运行时数据区:解析 它包含 五大模块 (方法区 虚拟机栈 本地方法栈 堆 程序计数器)
虚拟机栈(线程私有)
执行一个方法时,在虚拟机栈创建一个栈帧,用于存储该方法的局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用到执行结束就对应着一个栈帧从虚拟机进栈出栈的一个多过程。方法的这种执行机制天然适合使用栈的数据结构去存储。
用短代码解释下栈帧的结构和流程:
public int test() {
int a = 5;
int b = 6;
int c = a + b;
return c;
}
// 使用javap -c xx.class 反汇编生成的.class文件可以看到程序的运行指令,截取下面一段解释
// 指令手册可参考 https://www.jianshu.com/p/0978d7ab7113
// 局部变量表用来存储a,b,c三个变量
public int test();
Code:
0: iconst_5 //将操作数5压如操作数栈
1: istore_1 //操作数栈出栈赋值给局部变量表1,就是a
2: bipush 6 // 压入一个操操作数6
4: istore_2 // 操作数出栈赋给变量2,就是b
5: iload_1 // 将第一个int型变量就是a放到操作数栈
6: iload_2 // 将第二个int型变量就是b放到操作数栈
7: iadd // 将操作数栈顶两个元素相加并放到栈顶
8: istore_3 // 操作数栈出栈赋给变量3就是c
9: iload_3 // 将第三个int型变量就是c放到操作数栈
10: ireturn // 返回 方法执行完要知道返回到哪里,所以要记录方法出口,知道出口就知道要返回上层调用处然后继续执行下面的流程。
// 看下每行代码前有个行号,可以理解成程序计数器存放到就是这些行号,记录程序执行位置。系统执行多线程是通过时间片轮询的方式分配cpu的,所以当下一次分配到这个方法时,根据程序计数器就能知道执行到哪了,然后继续执行。
程序计数器(线程私有)
记录线程执行指令的位置,当cpu分配到当前线程时可以知道继续执行哪一条指令。
本地方法栈(线程私有)
区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
堆(线程共享)
对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
方法区(元空间 线程共享)
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
了解下堆外内存DirectByteBuffer
非虚拟机运行时数据区的部分,不属于以上任何区,占用本机内存。
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
Java堆,垃圾收集机制
空间分配一般是年轻代:老年代=1:2,eden:survivor1:survivior=8:1:1
年轻代使用 复制算法收集垃圾,老年代使用标记整理算法。
内存分配与回收策略
- 对象先进入Eden区(大对象,长期存活的对象进入老年代)
- Eden区满了触发一次minor gc,清理Eden区,将存活对象移入s0区,年龄为1
- Eden区再满了以后清理Eden和s0区,将存活对象复制到s1区,年龄2
- Eden区满了以后再清理Eden和s1区,将存活对象移入s0区,年龄3,所以s0和s1互相复制,总有一个为空。
- 下一次minor gc,survivor区满了,会将年龄大于15的对象放到老年代。(特殊情况也会有小于15的对象进入老年代)
- 老年代满了进行FullGC(stop the world)