本文参考了《尚硅谷宋红康JVM全套教程(详解java虚拟机)》的笔记与JavaGuide整理的笔记。
jvm将虚拟机分为 5大区域 ,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
程序计数器主要有两个作用:
⚠️注意 :程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
PC 寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。
Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用,栈是线程私有的
对于栈来说不存在垃圾回收问题, 不需要GC,但是可能存在OOM
StackOverFlowError
: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError
: Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的。
虚拟机栈由一个个栈帧组成,每个栈帧中存储着:
局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress 返回值类型。
作用:操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError
和 OutOfMemoryError
两种错误。
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
在 HotSpot 中,Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1
堆这里最容易出现的就是 OutOfMemoryError
错误,并且出现这种错误之后的表现形式还会有几种,比如:
java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过 -Xmx
参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap size)
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。
对象在 Survivor 区中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁,其实每个 JVM、每个 GC 都有所不同)时,就会被晋升到老年代
对象晋升老年代的年龄阀值,可以通过选项**-XX:MaxTenuringThreshold**来设置
针对不同年龄段的对象分配原则如下所示:
-XX:HandlePromotionFailure
。下一次再进行 GC 的时候:
1、这一次的 s0 区为空,所以成为下一次 GC 的 S1 区 2、这一次的 s1 区则成为下一次 GC 的 S0 区 3、也就是说 s0 区和 s1 区在互相转换。
关于垃圾回收(GC):频繁在新生区收集,很少在养老区收集,几乎不在永久区 / 元空间收集。
对象分配的特殊情况:
TLAB(Thread Local Allocation Buffer)
1 .从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内。 2. 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
TLAB 分配过程
方法区可以看作是一块独立于 Java 堆的内存空间。
方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式
方法区(Method Area)与 Java 堆 一样,是各个线程共享的内存区域。多个线程同时加载同一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区主要存放的是 Class,而堆中主要存放的是实例化的对象 在Java中有两种对象: Class对象 和 实例对象 ,实例对象是类的实例,通常是通过new关键字构建的。Class对象是JVM生成用来保存对象的类的信息的。
Java程序执行之前需要经过编译、加载、链接和初始化这几个阶段,编译阶段会将源码文件编译为.class字节码文件,编译器同时会在.class文件中生成Class对象,加载阶段通过JVM内部的类加载机制,将Class对象加载到内存中。
在创建对象实例之前,JVM会先检查Class对象是否在内存中存在,如果不存在,则加载Class对象,然后再创建对象实例,如果存在,则直接根据Class对象创建对象实例。JVM中只有一个Class对象,但可以根据Class对象生成多个对象实例。
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
当元空间溢出时会得到如下错误:
java.lang.OutOfMemoryError: MetaSpace
你可以使用 -XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited
,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace
将根据运行时的应用程序需求动态地重新调整大小。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 1OutOfMemoryError1 错误。
字符串常量池 StringTable 为什么要调整位置?