之前的文章有讲过,Java 源代码文件经过编译器编译后会生成字节码文件,并且经过类加载器加载之后会交给执行引擎执行,并在执行过程中,Java会划出一片空间去存储执行期间所用到的数据,这片空间成为运行时数据区。
根据 Java 虚拟机规范的规定,运行时数据区可以分为以下几个部分:
堆是所有线程共享的,对象的实例保存在这。几乎所有对象都会在堆中分配。
但随着 JIT编译器的发展和逃逸技术的逐渐成熟,所有的对象都分配到堆上渐渐变得不那么“绝对”了。从 JDK 7 开始,Java 虚拟机已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
类的信息,包括类的方法代码,变量名,方法名,访问权限,返回值等等。例如,我们调用getName方法 来获取类的名,数据来自于方法区。
方法区是一个逻辑区域,不同JDK有不同的规范。
可以看到,不同JDK版本,方法区的内容是不同的。
在JDK1.6时,常量池全都放进方法区之中。
但是在JDK1.7的时候,字符串常量池单独剥离出来,放进了堆中,并且此时方法区被称为永久代。方法区和永久代的关系就像是 Java 中接口和类的关系,类实现了接口,接口还是那个接口,但实现已经完全升级了。
JDK8之后,方法区废除了永久代,取而代之的时元空间的概念。
旧版的 Hotspot 虚拟机是没有 JIT 的,而 Oracle 旗下的另外一款虚拟机 JRocket 是有的,那为了将 Java 帝国更好的传下去,Oracle 就想把庶长子 JRocket 的 JIT 技术融合到嫡长子 Hotspot 中。
但 JRockit 虚拟机中并没有永久代的概念,因此新的 HotSpot 索性就不要永久代了,直接占用操作系统的一部分内存好了,并且把这块内存取名叫做元空间。
元空间的大小不再受限于 JVM 启动时设置的最大堆大小,而是直接利用本地内存,也就是操作系统的内存。有效地解决了 OutOfMemoryError 错误。
值得注意的是,当元空间的数据增长时,JVM 会请求操作系统分配更多的内存。但是如果也会发生内存溢出的问题。
虚拟机栈中保存的时虚拟机中的执行的方法的栈帧。每有一个方法被调用,其栈帧就会被压入虚拟机栈中。当方法执行结束,栈帧就会被弹出从虚拟机栈中。
注意:
虚拟机栈是每个线程独有的,不共享。
此外,虚拟机栈是会发生栈溢出的问题的。如果发生将会抛出 StackOverflowError。
本地方法栈和虚拟机栈类似,只不过虚拟机栈对应的方法是虚拟机中的方法,而本地方法栈 对应的是虚拟机用到的本地Native方法。
程序计数器记录的是下一条指令的地址。他所占的内存比较小。像分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
在线程并发的环境中,如果是单核,那么在同一时间,只能处理一个线程。如果时间片到期,就需要切换线程,这个线程的切换就需要程序计数器。另外,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。
也就是说,我们要求程序计数器是线程私有的。
之前在方法区中提到了字符串常量池,和运行时常量池(就是图中JDK1.7之后的常量池)。我们来看一下。它的作用是存放字符串常量,也就是我们在代码中写的字符串。依然在堆中。
就是字节码文件的资源仓库。在运行时,JVM 会将字节码文件中的常量池加载到内存中,存放在运行时常量池中。 常量池是在字节码文件中,而运行时常量池在元空间当中。