目录
1.JVM整体架构
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
名称 | 特征 | 作用 | 配置参数 | 异常 |
---|---|---|---|---|
程序计数器 | 占用内存小,线程私有,生命周期与线程相同 | 存储字节码运行号 | 无 | 无 |
虚拟机栈 | 线程私有,生命周期与线程相同,使用连续的内存空间 | Java 方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息 | -Xss | StackOverflowError OutOfMemoryError |
堆 | 线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址 | 保存对象实例,所有对象实例(包括数组)都要在堆上分配 | -Xms -Xsx -Xmn | OutOfMemoryError |
方法区 | 线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址 | 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 | -XX:PermSize:16M -XX:MaxPermSize64M -XX:MetaspaceSize=16M -XX:MaxMetaspaceSize=64M | OutOfMemoryError |
本地方法栈 | 线程私有 | 虚拟机使用到的Native方法服务 | 无 | StackOverflowError OutOfMemoryError |
JVM分为五大模块: 类装载器子系统 、 运行时数据区 、 执行引擎 、 本地方法接口 和 垃圾收集模块 。
2.JVM运行时内存
Java 虚拟机有自动内存管理机制,如果出现面的问题,排查错误就必须要了解虚拟机是怎样使用内存的。
Java7和Java8内存结构的不同主要体现在方法区的实现
方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
通常使用的Java SE都是由Sun JDK和OpenJDK所提供。而该版本使用的VM就是 HotSpot VM。通常情况下,我们所用的java虚拟机指的就是HotSpot的版本。
JDK7 内存结构
JDK8 内存结构
针对JDK8虚拟机内存详解
JDK7和JDK8变化小结
线程私有的:
程序计数器
虚拟机栈
本地方法栈
线程共享的:
堆
方法区
直接内存(非运行时数据区的一部分)
对于Java8,HotSpots取消了永久代,那么是不是就没有方法区了呢?
方法区只是一个规范,只不过它的实现变了。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。
方法区Java8之后的变化
- 移除了 PermGen(永久代),替换为 Metaspace(元空间)
- 永久代中的 class metadata(类元信息)转移到了 native memory(本地内存,而不是虚拟机)
- 永久代中的 interned Strings(字符串常量池)和 class static variables(类静态变量)转移到了Java heap
- 永久代参数 (PermSize MaxPermSize) -> 元空间参数 (MetaspaceSize MaxMetaspaceSize)
Java8为什么要将永久代替换成Metaspace?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将 HotSpot 与 JRockit 合二为一,JRockit没有所谓的永久代。
2.1.PC程序计数器
PC寄存器,是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
程序计数器的特点
- 区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
- 当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
- 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
2.2.虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
public class StackDemo {
public static void main(String[] args) {
StackDemo sd = new StackDemo();
sd.A();
}
public void A() {
int a = 10;
System.out.println(" method A start");
System.out.println(a);
B();
System.out.println("method A end");
}
public void B(){
int b = 20;
System.out.println(" method B start");
C();
System.out.println("method B end");
}
private void C() {
int c = 30;
System.out.println(" method C start");
System.out.println("method C end");
}
}
2.2.1.什么是栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
2.2.2.局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
2.2.3.操作数栈
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
2.2.4.动态链接
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
动态链接的作用:将符号引用转换成直接引用。
2.2.5.方法返回地址
方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。 无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
2.3.本地方法栈
本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务, 而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
特点
- 本地方法栈加载native的但是方法, native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
- 虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
- 是线程私有的,它的生命周期与线程相同,每个线程都有一个。
在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:
(1) StackOverFlowError:线程请求的栈深度 > 所允许的深度。
(2) OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。
2.4.堆
2.4.1.堆的概念
对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。 Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。“几乎”是指从实现角度来看, 随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
堆的特点
(1) 是Java虚拟机所管理的内存中最大的一块。
(2) 堆是jvm所有线程共享的。 (堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer )
(3) 在虚拟机启动的时候创建。
(4) 唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
(5) Java堆是垃圾收集器管理的主要区域。
(6) 很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,现在收集器基本都采用分代收集算法,所以Java堆可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
(7) java堆计算机物理存储上不连续、逻辑上连续,也是大小可调节的(通过-Xms和-Xmx控制)。
(8) 方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
(9) 如果堆中没有内存完成实例的分配,且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常
堆的分类
现在垃圾回收器都使用分代理论,堆空间也分类如下:
在Java7 Hotspot虚拟机中将Java堆内存分为3个部分:
- 青年代 Young Generation
- 老年代 Old Generation
- 永久代 Permanent Generation
在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了。
2.4.2.年轻代和老年代
JVM中存储java对象可以被分为两类:
1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成 1个Eden Space和 2个Suvivor Space(from 和 to)。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
新生代和老年代堆结构占比
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生 代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to 以示区分。 默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域 是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
2.4.3.对象分配过程
JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关, 因此还需考虑GC执行完内存回收后是否存在内存碎片。
分配过程
1.new 的对象先放在 Eden区。该区域有大小限制
2.当 Eden区 填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(Minor GC),将 Eden区 中不再被其他对象引用的额对象进行销毁,再加载新的对象放到 Eden区
3.然后将 Eden区 中的剩余对象移动到 S0区
4.再次触发垃圾回收时,上次幸存下来的放在 S0区 的对象,如果没有被回收,就会放到S1区
5.如果再次经历垃圾回收,存货对象会重新返回 S0区,接着再去S1区。
6.累计次数到达默认的15次,这会进入 old 区。 可通过调整阈值 -XX:MaxTenuringThreshold=N 改变累计次数
7.old 区 内存不足时,会再次出发GC:Major GC 进行 old区 的内存清理
8.如果 old区 执行了 Major GC 后仍然没有办法进行对象的保存,就会报OOM异常
分配对象的流程:
2.4.4.堆GC
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:一种是部分收集器(Partial GC)另一类是整堆收集器 (Full GC)
部分收集器: 不是完整收集java堆的的收集器,它又分为:
- 新生代收集(Minor GC/Young GC): 新生代的垃圾收集
- 老年代收集(Major GC/Old GC): 老年代的垃圾收集 (CMS GC 单独回收老年代)
- 混合收集(Mixed GC): 收集整个新生代及老年代的垃圾 (G1 GC会混合回收, region区域回收)
整堆收集(Full GC): 收集整个java堆和方法区的垃圾收集器
年轻代GC触发机制:
- 年轻代空间不足,就会触发Minor GC, 这里年轻代指的是Eden区满
- Minor GC会引发 STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复
老年代GC (Major GC)触发机制:
- 老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC
- 如果Major GC , 内存仍然不足,则报错OOM
- Major GC的速度比Minor GC慢10倍以上.
FullGC 触发机制:
- 调用System.gc() , 系统会执行Full GC ,不是立即执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC进入老年代平均大小大于老年代可用内存
GC调优命令参考:https://blog.csdn.net/qq_42029989/article/details/126271506
2.5.元空间
在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
HotSpots取消了永久代,那么是不是也就没有方法区了呢?
当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。
Metaspace和永久代有什么不同的?
- 存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
- 存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
为什么要废弃永久代,引入元空间?
- 在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。
- 移除永久代是为融合HotSpot VM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代永久代会为GC带来不必要的复杂度,并且回收效率偏低。
废除永久代的好处
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。
- 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。
- 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。
2.6.方法区
方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类型信息、 常量、 静态变量、 即时编译器编译后的代码缓存等数据
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但些简单的实现可能不会选择去进行垃圾收集或者进行压缩”。对HotSpot而言,方法区还有一个别名叫做Non-Heap(非堆),就是要和堆分开。
元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的
创建对象各数据区域的声明:
方法区的特点:
- 方法区与堆一样是各个线程共享的内存区域
- 方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续
- 方法区的大小跟堆空间一样 可以选择固定大小或者动态变化
- 方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类 导致方法区溢出虚拟机同样会抛出 (OOM)异常(Java7之前是 PermGen Space (永久代) Java 8之后 是MetaSpace(元空间))
- 关闭JVM就会释放这个区域的内存
2.6.1.方法区内部结构
类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
方法区中存储的内容:
- 类型信息(域信息、方法信息)
- 运行时常量池
类型信息
对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(全名 = 包名.类名)
- 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)
- 这个类型的修饰符( public, abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
域信息
域信息,即类的属性,成员变量,JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
- 方法名称方法的返回类型(或void)
- 方法参数的数量和类型(按顺序)
- 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
- 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
- 异常表(abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏 移地址、被捕获的异常类的常量池索引
2.7.运行时常量池
常量池与运行时常量池的区别
常量池:存放编译期间生成的各种字面量与符号引用
运行时常量池:常量池表在运行时的表现形式,编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。
字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠 JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。
它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。
对字节码文件反编译之后,查看常量池相关信息:
要弄清楚方法区的运行时常量池,需要理解清楚字节码中的常量池。
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant pool table),包括各种字面量和对类型、域和方法的符号引用。
常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
常量池表Constant pool table:
在方法中对常量池表的符号引用
为什么需要常量池?
举例来说:
public class Solution {
public void method() {
System.out.println("are you ok");
}
}
这段代码很简单,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,采取加载。
2.8.直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方 式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在Java堆和Native堆中来回复制数据。
NIO的Buffer提供一个可以直接访问系统物理内存的接口——DirectBuffer。
DirectByteBuffer继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制。而DirectByteBuffer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuffer时,系统总是会使用一个“内核缓冲区”进行操作。 而DirectByteBuffer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectByteBuffer是一种更加接近内存底层的方法, 所以它的速度比普通的ByteBuffer更快。
通过使用堆外内存,可以带来以下好处:
- 改善堆过大时垃圾回收效率,减少停顿。Full GC时会扫描堆内存,回收效率和堆大小成正比。Native的内存,由OS负责管理和回收。
- 减少内存在Native堆和JVM堆拷贝过程,避免拷贝损耗,降低内存使用。
- 可突破JVM内存大小限制。