天天看点

对JVM内存模型的一些理解

文章目录

    • JVM(Java virtual machine)
      • 1.JVM内存模型(JDK8)
      • 2.内存区域分类
      • 3.各区域的介绍及其作用详解
        • 3.1 程序计数器
        • 3.2 虚拟机栈
        • 3.3 本地方法栈
        • 3.4 堆
        • 3.5 方法区

JVM(Java virtual machine)

1.JVM内存模型(JDK8)

对JVM内存模型的一些理解

Java1.8的改动:

将永久代移除,取而代之的是元空间。

元空间不再与堆是连续的物理内存,而是改为使用本地内存(Native Method)。也就意味着只要本地内存足够,就不会出现OOM(OutOfMemory)的错误。

2.内存区域分类

对JVM内存模型的一些理解

线程私有的区域:程序计数器、虚拟机栈、本地方法栈

线程共享的区域:堆、方法区、直接内存

3.各区域的介绍及其作用详解

3.1 程序计数器

程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号的指示器。

如果线程正在执行的是一个Java方法,程序计数器记录正在执行的虚拟机字节码指令的地址;

如果正在执行的是Native方法,程序计数器的值为Undefined。

程序计数器的主要作用:

  • 在单个线程执行的过程中,字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、循环、异常执行。
  • 在多线程并发运行的情况下,程序计数器用于记录当前线程执行的位置,从而保证线程切换的时候能够从上一次的位置继续工作。

值得注意的是,程序计数器是JVM虚拟机规范中唯一一个没有规定OOM(OutOfMemory)Error的内存区域。

显然,由于程序计数器保存的是字节码指令的地址,因此它占用的空间为一个很小的定值。所以,这个区域不会出现OOM的情况。

程序计数器的生命周期同它所属于的线程的生命周期。

3.2 虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:

每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就是对应的栈帧在虚拟机栈中从入栈到出栈的过程。

对JVM内存模型的一些理解

局部变量表:

局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。

一个局部变量可以保存一个类型为boolean、byte、char、short、int、float和reference类型的数据。reference类型表示对一个对象实例的引用。

操作数栈:

操作数栈是数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。操作数栈可理解为虚拟机栈中的一个用于计算的临时数据存储区。

i++

:将局部变量表中的i压入操作数栈,将局部变量表中i自增,然后取栈顶的i使用。

++i

:将局部变量表中的i自增,然后将i压入操作数栈中,最后取栈顶的i使用。

这两个操作分为多步,不能保证原子性。

如何使其具有原子性?使用循环CAS操作。

动态链接:

每个栈帧中包含一个在常量池中对当前方法的引用。

方法返回地址:

方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入上层调用栈帧。
  • 异常信息抛给能够处理的栈帧。
  • PC计数器指向方法调用后的下一条指令。

虚拟机栈可能会出现两种错误:

StackOverFlowError

OutOfMemoryError

  • StackOverFlowError

    :若虚拟机栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出

    StackOverFlowError

  • OutOfMemoryError

    :若虚拟机栈的内存大小可以动态扩展,在虚拟机动态扩展栈的时候无法申请到足够的内存空间,就会抛出

    OutOfMemoryError

3.3 本地方法栈

和虚拟机栈所发挥的作用非常相似。

区别是: 虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

本地方法栈也会出现

StackOverFlowError

OutOfMemoryError

3.4 堆

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

JDK1.7之后,字符串常量池从方法区被移动到了堆中。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。

从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代。

再细致一点,新生代又可以分为Eden、From Survivor、To Survivor 空间等。

对JVM内存模型的一些理解

进一步划分的目的是更好地回收内存,或者更快地分配内存。

默认新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )

默认eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )

常见的堆设置:

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值

3.5 方法区

方法区与Java堆一样,是各个线程共享的内存区域。

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

JDK7之后,废弃了永久代的概念,改用在本地内存中实现的元空间(Metaspace)来代替。

-XX:MetaspaceSize=N //设置 Metaspace 的初始大小

-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

JDK7之后,字符串常量池被从方法区拿到了堆中,但是运行时常量池还在方法区之中。

class常量池和运行时常量池:

class常量池简介:

我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);

每个class文件都有一个class常量池。

什么是字面量和符号引用:

字面量包括:

1.文本字符串

2.八种基本类型的值

3.被声明为final的常量等;

符号引用包括:

1.类和方法的全限定名

2.字段的名称和描述符

3.方法的名称和描述符。

运行时常量池

存在于方法区中,也就是class常量池被加载到内存之后的版本,不同之处是:

它的字面量可以动态的添加

(如使用String#intern())

,符号引用可以被解析为直接引用

JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。