JVM虚拟机学习二、虚拟机内存模型(JDK1.8)

JVM虚拟机学习二、虚拟机内存模型(JDK1.8)

基于JDK1.8,HostPot虚拟机

JVM运行时内存模型(1.8)

在这里插入图片描述

先说说与JDK1.7之前的区别

  1. 没有了方法区,取而带之的是元空间(这么说不标准,方法区只是JVM规范的概念,并不实际存在。)
  2. 原来方法区中的运行时常量池中的字符串常量池,静态变量,都存在于堆中了。(注意是字符串常量池存放于堆中,并不是运行时常量池!)
  3. 方法区中其余的(例如类的加载信息)存在于元空间中,运行时元空间在存在于本地内存中的。

堆内存模型改变

jdk1.7之前,堆通常被划分为:新生代(Young Generation),老年代(Old Gerneration),永久代(Permannet Gerneration)。

在这里插入图片描述

​ jdk1.8之后堆空间将存放元数据的永久代从堆内存中移到了本地内存并改名为元空间(Metaspace)。元数据是数据的数据或者叫做用来描述数据的数据或者叫做信息的信息。(比如原本方法区存储的类信息、即时编译器编译后的代码等)

这么做的原因

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 不会再有java.lang.OutOfMemoryError: PermGen问题。
  3. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  4. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

在这里插入图片描述

永久代和方法区的关系:永久代其实就是方法区的实现,方法区只是JVM规范的一种概念,实际并不存在,本文都是以hostpot虚拟机为例,不同的虚拟机中方法区的实现在不同位置。

程序计数器(Program Counter Register)

​ 程序计数器(Program Counter Register),它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。

​ 由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的

虚拟机栈(Vitual Machine Stack)

​ Java栈也称作虚拟机栈(Java Vitual Machine Stack)事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。

​ 虚拟机中存放的是一个个栈帧,每一个栈帧对应着一个方法。栈帧包含有:局部变量表操作数栈动态链接方法的返回地址。**当程序的执行指令执行一个方法时,就会在虚拟机栈中创建一个对应的栈帧,并将其压入栈中,当方法执行完毕栈帧出栈。**因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。下图是虚拟机栈的模型:

在这里插入图片描述

局部变量表:根据名字就知道其含义:方法中的局部变量存放的位置(包括声明的非静态变量和方法的形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的

操作数栈:想栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的

动态链接:当方法中需要用到常量池中的常量时,该如何引用?这块区域就是用来存放常量池中常量的引用

方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

本地方法栈

​ 本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一

堆(Heap)

  1. JVM中所管理内存中的最大的一块。在虚拟机启动时被创建。
  2. 唯一的目的是存放对象实例,几乎所有的对象实例和数组都是在这里分配内存。
  3. 堆是垃圾收集管理的主要区域,所以也会被称为"GC"。
  4. 原来方法区中的运行时常量池中的字符串常量池,静态变量,都存在于堆中了

​ Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。

方法区(jdk1.8/hostpot)

​ 首先关注一个各个版本的方法区中的一些改变:

  1. 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
  2. 在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
  3. 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)

​ 方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

运行时常量池、字符串常量池、静态常量池

1.静态常量池(class文件常量池)

​ 我们都知道class文件中除了包含类的版本、字段、方法、接口等信息,还包含有常量池:用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)

字面量:就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。

符号引用:是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
直接引用:直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置,引用的目标必定已经被加载入内存中了

2.字符串常量池(String pool)

​ 全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。
​ 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

3.运行时常量池(runtime constant pool)

​ 当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

​ jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。运行时常量池逻辑上是包含字符串常量池的

​ 总结:

  • 1.全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。
  • 2.class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
  • 3.运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

有关String中的intern()的解读:https://blog.csdn.net/qq_38238296/article/details/89335678

元空间(metaspace)

​ 在JDK1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了MetaSpace(元空间)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

部分转载自:

http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/

http://www.cnblogs.com/dolphin0520/p/3613043.html

https://www.cnblogs.com/cjsblog/p/9850300.html

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值