jvm系列(1)内存结构(补充版)

在一开始学习java的时候,那时候是在网上看视频,老师就常常提到什么对象分配在堆区,什么在栈区,那时候和理解,后来理解了就想着写一篇文章好好的去梳理一下。
java

想说一下这篇文章的脉络:数据结构

首先,研究java7的内存结构,并对其进行一个详细的介绍,由于理解了java7以后java8比较容易理解多线程

接下来,使用一个例子来详解咱们在运行一个程序的时候,代码在java虚拟机中的存储和转化。app

最后,咱们给出java8的内存结构,看一看作了哪些改动,并和java7进行一个比较。jvm

第一部分:java7内存结构

先给一张java7的内存结构图吧(我用Windows里面的画图工具画的,因此看起来不怎么美观)ide

图片

首先对这个图有一个认识,从上面能够看到java7的内存结构大体分了五个部分:PC寄存器,java虚拟机栈、本地方法栈、java堆、方法区。其中PC寄存器、java虚拟机栈和本地方法栈是全部线程共享的一块内存区域。java堆和方法区是每个线程隔离的一块区域,其中,方法区还有一个运行时常量池。
工具

接下来看一看每一块区域里面存放的什么?学习

1、PC寄存器测试

在大学的时候学过计算机组成原理的时候都知道,内存里面有不少寄存器,大概几百个吧(目前的,以前大学学的时候老师说才几十个),每一种寄存器的用途都不同,其中有一个寄存器就是程序计数器。这个寄存器的主要做用就是存放下一条须要执行的指令。优化

首先,为何要有这个程序计数器呢?这是由于咱们的处理器在一个时刻,只能执行一个线程中的指令。可是咱们的程序每每都是多线程的,这时候处理器就须要来回切换咱们的线程,为了在线程切换以后回到以前正确的位置上,此时就须要一个程序计数器,这也就很容易理解了咱们的每一个线程都有一个本身的程序计数器来保存本身以前的状态。

接下来如何理解这个程序计数器的功能呢?假如咱们的程序代码假如是一行一行执行的,程序计数器永远指向下一行须要执行的字节码指令。在循环结构中,咱们就能够改变程序计数器中的值,来指向下一条须要执行的指令。所以,在分支、循环、跳转、异常处理和线程恢复等等一些场景都须要这个程序计数器来完成。

最后看一下在什么状况下,应该存储什么内容。《java虚拟机规范》中说若是当前执行的是 Java 的方法,则该寄存器中保存当前执行指令的地址;假若执行的是native 方法,则PC寄存器中为空(Undefined)。PC寄存区区域就是存放了N多个这样的寄存区。此内存区域是惟一一个在Java虚拟机规范中没有规定任何OutOfMemoryError状况的区域。所以能够把他的几个特色概括以下。

  1. 程序计数器指定下一条须要执行的指令

  2. 每个线程独有一个程序计数器

  3. 执行java代码时,寄存器保存当前指令地址

  4. 执行native方法时候,寄存器为空。

  5. 不会形成OutOfMemoryError状况

2、Java虚拟机栈

每个线程都有本身的java虚拟机栈,这个栈与线程同时建立,一个线程中的每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。每一个线程有一个私有的栈,随着线程的建立而建立。栈里面存着的是一种叫“栈帧”的东西,每一个方法会建立一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操做数栈、动态链接和返回地址等信息。当前运行方法对应的栈帧叫作当前栈帧。下面主要对这个栈帧进行一个介绍。

先看一张图

图片

首先,局部变量表里存放了编译期间可知的各类基本数据类型(8种)、对象引用、returnAddress类型(指向一条字节码指令的地址)。他有以下特色:

  • 64位长度的long和double类型占用2个局部变量空间(Slot),其他数据类型只占用一个。

  • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法须要在帧中分配多大的局部变量空间是彻底肯定的,

  • 在方法运行期间不会改变局部变量表的大小。

       接下来操做数栈,其实在栈帧刚刚建立的时候,操做数栈是空的,java虚拟机能够从局部变量表或者对象的实例字段中,复制一些常量或者变量值到操做数栈中。也能够从操做数栈中取走数据。他的深度在编译期就已经肯定了。

     动态链接是什么意思呢?在这里咱们先有个基本的印象,下面举例子的时候,再来看这个解释比较容易理解一点,咱们知道,在线程中一个方法去调用另一个方法,是经过符号引用来实现的,动态链接的做用就是把这个符号引用表示的方法转化为实际方法的直接引用。

对于java虚拟机栈的描述,最后看一下可能发生的异常状况:

  • 若是线程请求分配的栈容量超过java虚拟机栈所容许的最大容量,java虚拟机就会抛出StackOverfolwError

  • 若是java虚拟机栈动态扩展,在扩展时没有申请到足够的内存或者是建立新线程时没有足够的内存再建立java虚拟机栈了,那么java虚拟机就会抛出outOfMemoryError

3、本地方法栈(Native Method Stack)

与虚拟机栈相似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,所以虚拟机能够自由实现它。本地方法栈能够抛出StackOverflowError和OutOfMemoryError异常。不过这块区域咱们不怎么去关心。

4、Java堆

Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立,用来存放对象实例。是内存中最大的一块区域。垃圾收集器(GC)在该区域回收不使用的对象的内存空间。可是并非全部的对象都在这保存,深刻理解java虚拟机中说道,随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量调换优化技术将会致使一些微妙的变化,全部的对象都分配在堆上也逐渐变得不那么绝对了。

堆的大小能够固定也能够动态扩展,可经过-Xms(最小值)和-Xmx(最大值)参数设置,若是在堆中没有内存完成实例分配,且堆也没法在扩展时,会抛出OutOfMemoryError异常。

下面给一张java 堆的结构图,

为了支持垃圾收集,堆被分为三个部分:

年轻代 : 经常又被划分为Eden区和Survivor(From Survivor To Survivor)区(Eden空间、From Survivor空间、To Survivor空间(空间分配比例是8:1:1)

老年代:

永久代 :(jdk 8已移除永久代,取而代之的是元空间。下面会讲解)

5、方法区

方法区也是全部线程共享。主要用于存储类的信息、常量池、静态变量、及时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分。一般又叫“Non-Heap(非堆)”。

第二部分:使用例子理解java7内存结构

一个例子理解所有

为了理解的比较深入,先给一个例子。经过例子讲解印象更加深入吧,假设咱们在idea或者是任何IDE环境中定义了一个类。

有一个person类

public class Person{
int age;
String name;
Baby baby;
public void walk() {
System.out.println("我正在走路。。。。");
}
}

还有个Baby类

public class Baby{
   String babyname;
   int babyAge;
   public void cry(){
       System.out.println("我是孩子,我会哭");
  }
}

最后是一个测试类Test

public class Test {
public static void main(String[] args) {
Person person = new Person();
person.name = "冯冬冬的IT技术栈";
person.age = 18;
person.walk();

Baby baby= new Baby();
baby.babyname = "冯XX";
System.out.println(baby.babyname);

person.baby = baby;
System.out.println(pserson.baby.cry);
}
}

好了有了上面的环境,接下来就开始分析这些代码在运行时内存的变化。如今在咱们的IDE开始运行。

  1. 第一步,JVM去方法区寻找Test类的代码信息,若是有直接调用,没有的话使用类的加载机制把类加载进来。同时把静态变量、静态方法、常量加载进来。这里加载的是(“冯冬冬的IT技术栈”,“冯XX”);这是由于字符串是常量,age中的18是基本类型。

  2. 第二步,jvm进入main方法,看到Person person=new Person()。首先分析Person这个类,一样的寻找Person类的代码信息,有就加载,没有的话类加载机制加载进来。同时也加载静态变量、静态方法、常量(“我正在走路。。。”)

  3. 第三步,jvm接下来看到了person,person在main方法内部,于是是局部变量,存放在栈空间中。

  4. 第四步,jvm接下来看到了new Person()。new出的对象(实例),存放在堆空间中。

  5. 第五步,jvm接下来看到了“=”,把new Person的地址告诉person变量,person经过四字节的地址(十六进制),引用该实例。 是否是有点晕,别着急,画个图看一下。

    图片

  6. 第六步,jvm看到person.name = "冯冬冬的IT技术栈";person经过引用new Person实例的name属性,该name属性经过地址指向常量池的"冯冬冬的IT技术栈"。

  7. 第七步,jvm看到person.age = 18; person的age属性是基本数据类型,直接赋值。

  8. 第八步,jvm看到person.walk(); 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是经过地址指向方法区中类信息的方法。走到这一步再看看图怎么变化的。

    图片

  9. 第九步,jvm看到Baby baby=new Baby().这个过程和Person person = new Person()同样

  10. 第十步,jvm看到baby.babyname = "冯XX";这个过程也和person.name = "冯冬冬的IT技术栈";同样。

  11. 第十一步,jvm看到person.baby = baby;把baby对象引用赋值给Person实例的baby属性属性。

好了,到了这一步,应该对java7的内存结构有一个详细的认识了。

第三部分:java8内存结构

其实在第一部分的方法区介绍里面,已经提早说了一些,想要好好的理解java8内存结构,那必定是在java7的基础上和其做比较,所以首先解释一下两个名词:永久代(PermGen)和元空间(Metaspace)。

首先是永久代:

咱们常见的 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,然后者则是 JVM 规范的一种实现,而且只有 HotSpot 才有 “PermGen space”。因为方法区主要存储类的相关信息,因此对于动态生成类的状况比较容易出现永久代的内存溢出。

而后是元空间

元空间的本质和永久代相似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。所以,默认状况下,元空间的大小仅受本地内存限制。

先给出java8的内存结构图。

图片

须要注意内存模型与内存结构不一样

在内存结构中,其中Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可能能够操做保存在堆或者方法区中的同一个数据。

在内存模型中,其实JMM并非是真实存在的。他只是一个抽象的概念。咱们知道在多线程通讯时候会存在一系列如可见性、原子性、顺序性等问题,而JMM就是针对这些问题而创建的模型。

图片

1、java7到java8的第一部分变化:元空间

下面来一张图看一下java7到8的内存模型吧(这个是在网上找的图,若有侵权问题请联系我删除。)

图片


2、java7到java8的第二部分变化:运行时常量池

运行时常量池(Runtime Constant Pool)的所处区域一直在不断的变化,在java6时它是方法区的一部分;1.7又把他放到了堆内存中;1.8以后出现了元空间,它又回到了方法区。

相关文章
相关标签/搜索