本文知识点

  • JVM虚拟机制定的规范

  • 方法区,永久代,元空间的区别

总述

在这一块的学习时, 我们容易陷入一个误区,就是一上来就直接搜索运行时数据区, 网上有些文章对虚拟机规范和HOTSPOT实现没有区分开,导致有时候大家看的两篇文章解释尽不一样. 自己也容易糊涂. 所以本篇特地将两个拆开讲. 且尽量以官方文档为准

我们可以把jvm规范理解成接口. 就是要这些东西, 然后不同的虚拟机厂商有不同的实现方案. 如方法区,hotspot 用了1.7及以前用了永久代, 1.8及以后用了元数据区. 别的虚拟机如JRocket,J9 都没有永久代的概念.

JVM运行时数据区制定的虚拟机规范

如上参考pdf中及下图所示,主要有六大数据区域:

The pc Register | program Counter Register | 程序计数器

程序计数器为线程私有的,每个线程都有自己独立的程序计数器

如果当前线程执行的是Java方法,则程序计数器中是当前执行虚所机字节码指令的地址,如果正在执行是native方法,这个计数器的值是空的

我们假设有以下场景. 此时有两个线程A,B正在执行.

CPU执行线程A 的 Ia1指令时, Ta 的程序计数器存的是Ia1指令的地址, 执行完指令Ia1后,转而执行线程B的Ib1指令, 再回到线程A时, 从程序计数器中取出上次执行到了Ia1, 然后继续往下执行.

Java Virtual Machine Stacks | java虚拟机栈

java虚拟机栈也是线程私有的, 该线程每调用一个方法,都用创建一个栈帧(Frame).栈帧中有局部变量表,操作数栈,动态链接,方法出口等信息.

开发中遇到和虚拟机栈相关的问题:

  1. i++ 线程不安全

    我们常说的i++线程不安全问题, 其根本就在于栈帧中的局部变量表,操作数栈这两个结构.

  2. 递归太多StackOverFlow

    递归就是自己调用自己,每调用一次,就是创建一个栈帧添加到虚拟机栈中, 添加的多了,超过了容量,就会报如上StackOverFlow的错误

我们在idea的debug界面也可以看到关于栈和栈帧相关界面,如下图所示:

1:栈帧列表

2:可以切换不同的线程,看对应的栈帧

3:当前栈帧中的用到的变量

Heap 堆

所以线程共享的一块区域,几乎所以有java对象都在堆里面进行分配,这里要注意以下几个问题

  1. 并不是所有的对象都在堆中分配

    这是一个很容易被忽略的点,jdk1.8之后,虚拟机默认开启子逃逸分析,如果变量A只在本方法中使用,则可以不在堆中分其分配,可以在栈中为其分配. 这样随着方法调用结束,栈帧销毁,对角也跟着销毁,就不用调用GC了

  2. 虚拟机规范并没有对堆进行分代划分

    如我们现在常说的年轻代,老年代等是HotSpot的实现, JVM规范只是制定了堆,没有制定分代的标准.

 

Run-Time Constant Pool |  运行时常量池

运行时常量池是方法区的一部分,与之对应是.class文件中的静态常量信息,如下图所示:

在class文件加载的链接步骤中的解析阶段,会把静态的常量池和运行时常量池关联起来,把符号引用变成直接引用.

Method Area |方法区

方法区也是被线程所共享的,其实是从堆里面划出来的一片区域(这里不要钻是从哪个代里面划出来的, 如上据说,JVM规范并没有规定分代的,由各个实际的虚机机去实现的,可自己去看怎么划分)

里面存放的有:已被虚拟机加载的类信息, 常量,静态变量,即时编译器编译后的代码缓存

Native Method Stacks | 本地方法栈

这个和上面的java虚拟机栈没太大的差别, 在jvm规范层面,把本地方法栈描述为在java调用其他语言写的方法时创建,在HotSpot实现层面, 直接把本地方法栈和虚拟机栈合二为一. (所以说规范和实现要分开学习)

方法区,永久代,元空间(MetaSpace)的区别

总的来说, 方法区是接口, 永久代和元空间是实现

在HotSpot中,1.7及以前的版本以永久代做为方法区的实现, 1.8及以后版本的jdk以MetaSpace做方法区的实现.

永久代在堆里面, MetaSpace直接使用了直接(本地)内存.

相应的1.8及以后. 移除了永久代,以MetaSpace做方法区实现,常量池中的字符串常量池,直接留在了堆中. 其他的如类信息,静态信息留在了MetaSpace中,也跟着去了直接(本地)内存