走进JVM的内存模型

1、概述:我们在用Java语言进行编程时,并没有像C/C++程序这样为每一个 new 操作去写对应的 delete/free操作。这得益于Java 程序把内存控制权利交给 JVM虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

2、JVM内存模型:

JVM虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

JDK 1.8 和之前的版本略有不同,JDK 1.7

走进JVM的内存模型_第1张图片

JDK 1.8之前分为:线程共享(Heap堆区、Method Area方法区)、线程私有(虚拟机栈、本地方法栈、程序计数器)

JDK 1.8

走进JVM的内存模型_第2张图片

JDK 1.8以后分为:线程共享(Heap堆区、MetaSpace 元空间)、线程私有(虚拟机栈、本地方法栈、程序计数器)

下面我们将划分的每个区域进行更进一步了解:

3、程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

作用:

     1、字节码解释器通过改变程序计数器读取指令,从而实现代码的流程控制

     2、多线程在线程上下文切换时,用于记录当前线程执行的位置

4、Java虚拟机栈

与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。在活动线程中, 只有位于栈顶的栈帧才是有效的, 称为当前活动栈帧,代表正在执行的当前方法。在JVM执行引擎运行时, 所有指令都只能针对当前活动栈帧进行操作。

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。            走进JVM的内存模型_第3张图片                              走进JVM的内存模型_第4张图片

局部变量表:存放了编译期可知的各种基本数据类型、对象引用。

操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接 主要服务一个方法需要调用其他方法的场景。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接

Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

简单总结一下程序运行中栈可能会出现两种错误:

  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError错误。
  • OutOfMemorryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemorryError异常。

5、本地方法栈

与Java虚拟机栈的功能类似,它负责native关键字修饰的本地方法被执行的时候,在本地方法栈中创建一个栈帧,用于存放该native本地方法的局部变量表、操作数栈、动态链接、方法出口信息。方法执行完毕后,相应的栈帧也会出栈并释放内存空间。也会出现StackOverFlowError和 OutOfMemoryError两种错误。

以上是线程私有的区域,下面两个区域则是线程共享区:元空间与堆区

6、堆区

Heap堆是JVM 所管理的内存中最大的一块区域,被所有线程共享的一块内存区域。堆区中存放对象实例,“几乎”所有的对象实例以及数组都在这里分配内存。

由于“逃逸分析”的存在,若某些方法中的对象引用没有被返回或者未被外面使用,就会在栈分配。

堆区是JVM进行垃圾回收最主要的一片区域,也常称GC堆,为了更好的管理与垃圾回收,进一步划分为新生代(Eden,S0、S1)与老年代。

新生代:老年代=1:2;Eden:Eden:S0+S1=8:2

走进JVM的内存模型_第5张图片

对象的创建过程:

步骤一:类加载检查

        

步骤二:分配内存

        

步骤三:初始化零值

        

步骤四:设置对象头

        

步骤五:执行init构造方法

        

上面说大部分对象的创建都会在堆区进行分配空间,那么对象究竟是如何分配的?
大部分情况下,对象会在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection,即 YGC垃圾回收的时候,在 Eden 区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到 Survivor 区。Survivor 区分为 s0 和 s1 两块内存区域。每次 YGC的时候,它们将存活的对象复制到未使用的Survivor 空间(s0 或 s1),然后将当前正在使用的空间完全清除,交换两块空间的使用状态。每次交换时,对象的年龄会加+1。
如果 YGC 要移送的对象大于 Survivor 区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,在 JVM 中 一个对象从新生代晋升到老年代的阈值默认值是 15,可以在 Survivor区交换 14 次之后,晋升至老年代。

7、元空间

在jdk1.8后使用元空间替换了永久代,看到这有点迷糊了,方法区、永久代、元空间三者到底什么关系?

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

你可能感兴趣的:(jvm,java,开发语言)