JVM 内存模型学习笔记

JVM 内存模型学习笔记

文章目录

  • JVM 内存模型学习笔记
    • 概述
      • 为什么需要学习 JVM 内存模型
    • 5 块内存的分类和区别
      • 程序计数区
      • 虚拟机栈
        • 局部变量表
        • 操作数栈
        • 动态链接
        • 方法出口
      • 本地方法栈
      • 方法区
        • 类的常量池
        • 运行时常量池
        • 字符串常量池
    • 拓展
      • String 的一些关于内存的问题
      • 常用类型及其包装类
    • 参考资料

概述

为什么需要学习 JVM 内存模型

为什么需要学习 JVM 内存模型呢?很简单,同样的需求,不同的人实现下来,最后的效果不一样,有的人写得程序很卡顿,有的人写的程序就很流畅。这之间有什么差距呢?

程序写的好的人,一般对一些细节了解的比较好,能从较多的实现方式里面找到比较好的解决方法,而程序写的不是很好的人一般不了解细节,许多需求只是“实现了”,但是并不是很好的实现方式,所以写的不好。

了解一些开发细节可以帮助我们在写代码的时候考虑到这些细节,从而避免不好的实现方式。让我们写出更好的程序。

5 块内存的分类和区别

通常来讲,我们一般习惯把 JVM 的内存划分为 5 块,不同的内存块有不同的作用,接下来详细介绍下。

程序计数区

所谓的程序计数区是一块比较特别的内存区域。我们做开发的都知道 ,程序是一行一行的代码执行的结果,那 CPU 下一秒执行哪一行代码呢?这个答案就需要问程序计数器了。程序计数器可以理解为当前线程的执行字节码的行号指示器。在程序运行的过程中, JVM 通过修改这个计数器的值达到控制程序运行的目的。

需要注意的是

  • 每个线程都有自己独立的程序计数器,即程序计数器是线程私有的,因为每个线程的执行是相互独立的。

  • 程序计数器是 Java 虚拟机规范中没有规定任何 OutOfMemoryError 的内存区域。

  • 在执行 Java 方法的时候,记录的是虚拟机字节码的地址。如果执行的是 Native 方法,则计数器的值为空( Undefined )。

虚拟机栈

在上一小节我们提到了代码的执行,而代码大部分是写在方法里面的。Java 虚拟机通过执行,调用不同的方法,最终运行了程序。

方法的执行、调用等就涉及到了虚拟机栈。虚拟机栈表明它是一个栈,栈里面的元素我们称之为栈帧。每个方法调用的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

局部变量表

局部变量表中存放的是方法参数和临时变量。方法参数可能是基础类型,可能是一个引用,可能是一个对象的句柄,还有就是方法的返回地址。

局部变量表的第一个元素是 this ,也就是当前的线程变量。

这个区域会有 2 中错误。

  • 栈深度大于 JVM 虚拟机的最大栈深度,此时会抛出 StackOverflowError 。这个深度受虚拟机限制。

栈深度可以理解为方法的调用链,即方法调用链有一定深度,不可以无限调用方法。

  • 动态扩展(解除上面说的栈的深度受虚拟机的限制,但不是无限的)的时候,超过允许的最大长度的时候,会抛出 OutOfMemoryError 异常。

操作数栈

首先操作数栈是一个栈,里面存放的是计算的中间结果。,具体的可以参考 【java虚拟机】栈帧、局部变量表、操作数栈和【转】【java虚拟机】栈帧、局部变量表、操作数栈

动态链接

动态链接就是在加载 class 的时候,将 class 的方法、字段和具体内存布局做一个链接。这个链接的关系我们称为动态链接。

方法出口

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

本地方法栈

本地方法栈与虚拟机栈的功能类似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。本地方法栈就是一个C的方法栈,本地方法栈的参数顺序、返回值和典型的C程序相同,本地方法一般来说可以(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种native方法调用Java会发生在栈(一般是Java栈)上,线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。

方法区

首先,方法区保存的是已加载的类的信息、常量、静态变量和编译后的一些代码。除此之外,还有一个很重要的部分就是运行时常量池。

类的常量池

class 文件除了包含 jvm 版本信息,字段、方法、接口等描述信息外,还有一个常量池来记录编译器生成的字面量符号引用

字面量就是文本字符串和被声明为 final 的常量的值。

符号引用就是一组符号,这个符号可以唯一定位到所引用的目标。符号可以是任何字面量。一般包含下面三类:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

运行时常量池

class 文件里的常量池由于是符号引用,无法直接找到对应的引用目标(内存里面的实际地址),在 class 被载入后,我们才能知道某个方法的实际引用目标。类的常量池在载入后的这个转化会被放在一块叫做运行时常量池的地方。

运行时常量池除了可以存放转化后的累的常量池,还可以存放运行期新生成的常量。

具体详情可参考下面的 2 篇文章。

  • Java——常量池探索
  • 字符串常量池、class常量池和运行时常量池

JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

字符串常量池

待续

几乎所有的对象实例以及数组都是创建在堆上分配的内存。而不在堆上分配内存的情况可参考 Java中对象都是分配在堆上吗?你错了! 。

JIT 技术的优化

消除同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。逃逸分析可以判断出某个对象是否始终只被一个线程访问,如果只被一个线程访问,那么对该对象的同步操作就可以转化成没有同步保护的操作,这样就能大大提高并发程度和性能。
矢量替代。逃逸分析方法如果发现对象的内存存储结构不需要连续进行的话,就可以将对象的部分甚至全部都保存在CPU寄存器内,这样能大大提高访问速度。

GC 作用的区域基本就是这个区域。

拓展

String 的一些关于内存的问题

常用类型及其包装类

参考资料

可能是把Java内存区域讲的最清楚的一篇文章

【转】【java虚拟机】栈帧、局部变量表、操作数栈

深入了解 Java 之虚拟机内存

你可能感兴趣的:(Java-基础,jvm,java)