JVM学习笔记二:JVM内存

JVM学习笔记二:JVM内存

前言

上一节学习了JVM的一些基础知识,在计算机里有内存的概念,那么运行于计算机上的Java肯定也不例外,这一节我们就来学习一下JVM中的内存模型

JVM内存模型

在JVM中,内存大致会分为 五个区域,它们分别是:程序计数器、虚拟机栈、本地方法栈、堆内存 和 元数据空间,下面我们挨个来给大家介绍一下

未命名文件.png

在介绍之前,首先需要注意的是,由图所示:元数据空间与堆为线程共享区域,而虚拟机栈、本地方法栈和程序计数器则是线程隔离区域。也就是说在元数据空间或堆上的对象,只需要创建一次,并且所有线程均可访问;而在虚拟机栈和本地方法栈上,每个线程都会单独创建一个需要的对象

元数据空间

此处主要用来存储 已被加载的类信息、静态变量、常量和即时编译器编译出来的代码。当无法满足内存分配时会 内存溢出

虚拟机栈

  • 在虚拟机栈里,内存是以 一个一个的栈帧 的形式存在的,栈帧里包括 局部变量表、操作数栈、动态链接和方法返回地址 等信息
  • 每个Java方法在执行的时候都会创建一个栈帧,方法从执行开始到结束的过程就是栈帧在虚拟机栈中入栈出栈的过程
  • 局部变量表:存放一些编译期就能够知道的基本数据类型、对象引用和返回类型,其所需要的内存空间会 在编译期间完成分配。进入一个方法的时候,在栈帧中的局部变量表的空间是 完全确定 的,不需要运行时改变
  • 操作数栈:主要用于 保存方法中计算过程的中间结果,同时作为计算过程中变量的 临时存储空间,每个操作数栈都拥有一个明确的栈深度用于存储数值,其所需要的最大深度在编译期就已经定义好了,这个深度被报错在方法Code属性中,其为max_stack的值
  • 动态链接:每个栈帧都保存了一个可以指向当前方法所在类的运行时常量池,目的是为了在当前方法需要调用其他方法时,可以从运行时常量池中找到对应的 符号引用,接着将符号引用转换为直接引用(内存地址),然后就可以直接调用该方法,也就是方法中调用其他方法的链接方式
  • 方法返回:存放该方法在寄存器中的值,即是该方法的 指令地址,方便执行引擎在执行完该方法后回到其对应的指令行号,然后继续执行下去
  • 如果线程申请的栈深度大于虚拟机允许的最大深度,则会抛出 StackOverFlowError 的错误。虚拟机在动态扩展时,如果无法申请到足够的内存,则会抛出OutOfMemoryError 错误

本地方法栈

其栈与虚拟机栈基本相同,主要用来管理 native 方法,且其占用的内存大小是不固定的,可以根据需要来 动态扩展

堆中主要以 老年代新生代 两种形式存在

  • 新生代:一个 Eden区 和两个 Survivor区,在发生 Minor GC 的时候会把存活的对象拷贝到另一个 Survivor区 上,因此也成为 From区To区。对象优先在 Eden区 分配,大对象则直接进入 老年代,目的是为了避免 Eden区Survivor区 的互相拷贝大对象。(虚拟机提供了 -XX:PretenureSizeThreshold 参数,大于该参数设置的值的对象将直接在老年代分配)
  • 长期存活的对象会进入老年代;如果对象在 Eden区 分配并经过一次 Minor GC (Young GC) 依然存活,并能被 Survivor区 容纳,将对象复制到 Survivor区,同时将对象的年龄设置为1,对象在 Survivor区 经历过一次 Minor GC ,年龄增加到一定程度后(默认是15岁)就会晋升到老年代
  • Minor GC (Young GC)是指发生在新生代的 垃圾回收 动作,Java对象大多数都具备朝荣夕灭的特性,所以 Young GC 一般比较频繁,回收的速度也比较快,当 Eden 空间 不足以分配内存给新的对象时触发。
  • Major GC (Full GC)是指发生在老年代的 垃圾回收 动作,一般出现了 Major GC 也会伴随着 Minor GCFull GC 的速度一般要比 Young GC 慢10倍以上,触发条件为:老年代的空间不足,新生代对象或大对象无法转入老年代,大对象一般需要大的连续空间,如果直接进入老年代很容易会出现 Full GC,因此避免存活期短的大对象存在
  • 该区域 用于存放对象实例,几乎所有的对象实例都会在堆上分配,也是 GC管理的主要区域

程序计数器

  • 它是一块比较小的存储空间,它 存储着当前线程执行的字节码行号指示器
  • 它是一个 线程私有 的数据区,每个线程都有一个独立的程序计数器,随着线程的创建而创建,随着线程的销毁而销毁
  • 在JVM中,它是唯一一个 不会发生内存溢出 的区域
  • 它用于记录当前线程执行到代码的哪一行,若此时程序被挂起,则程序计数器会记录当前代码的位置,在线程恢复后从记录的位置开始执行

注意:我们常用的异常处理和分支操作等,都是通过程序计数器来完成的。

总结

未命名文件.png

JVM 中,内存共分为 五部分元数据空间虚拟机栈本地方法栈JVM堆程序计数器 。其中 元数据空间JVM堆线程共享的区域,而虚拟机栈本地方法栈程序计数器 均为 线程隔离的区域。

每个线程都拥有一个单独的 程序计数器 来记录代码执行的位置。同时也拥有独立的 JVM栈 ,也就是虚拟机栈本地方法栈

而线程就是在 中运行的,线程执行某一个方法,就会对该方法创建一个 栈帧,在栈帧里有这个方法的 局部变量表、操作数栈 和 链接等。

任何方法的调用,都要遵守 先入后出 的规则

在Java中,运行一段程序代码会经历以下历程

  1. 启动JVM进程
  2. 加载类加载器
  3. 将加载到的信息放到 JVM中对应的内存区域内
  4. 执行main方法
  5. 在main线程中将main方法入栈
  6. 如果需要创建实例对象,则此时会在JVM堆中创建
  7. 由栈中的局部变量引用这个堆内存实例对象的地址
  8. 执行不同方法的时候,依次遵循先入后出的规则
  9. 对象无引用,GC开始垃圾回收