简单理解JVM内存结构组成
一、什么是JVM
定义
Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境),是JRE的一部分,一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM就是为了支持与操作系统无关,实现跨平台性。
二、内存结构
整体包括:类装载器、运行时数据区、执行引擎、垃圾回收。
其中运行时数据区包括:方法区、堆、虚拟机栈、程序计数器、本地方法栈五部分组成。
如下图:
程序计数器
作用:用于保存jvm下一条要执行指令的地址。
特点:
-
线程私有
- CPU会为每个线程分配时间片,当当前线程时间片使用完以后,CPU会切换到另外的线程并执行其中的代码。
- 程序计数器是每个线程所私有的,当另外的线程时间片执行完以后,会返回来执行当前线程代码,通过程序计数器,就可以知道应该执行哪一条指令。
-
不会存在内存溢出
因为程序计算器所维护的就是下一条待执行的命令的地址,所以不存在OutOfMemoryError。
虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从调用直到执行完成的过程,就对于着一个栈帧在虚拟机栈中入栈到出栈的过程。
在 Java 虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可以动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
小结:
每个线程运行需要的内存空间,称为虚拟机栈
每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
内存溢出:Java.lang.stackOverflowError 栈内存溢出
发生原因:
1、虚拟机栈中,栈帧过多(无限递归);
2、每个栈帧所占用过大 ;
本地方法栈
一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作
系统底层交互,所以需要用到本地方法 。
堆
对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Colleted Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;新生代又被划分为 Eden 空间(伊甸园)、From Survivor 空间(幸存区from)、To Survivor(幸存区to) 空间等。
根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
小结:
通过new关键字创建的对象都会被放在堆内存
所有线程共享,堆内存中的对象都需要考虑线程安全问题
有垃圾回收机制
java.lang.OutofMemoryError :java heap space. 堆内存溢出
堆内存诊断工具:jps、jmap、jconsole、jvirsalvm
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它永固存储已被虚拟机加载的类信息、常量、静态常量、及时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
jdk7及以前:习惯上把方法区,称为永久代。
jdk8开始: 使用元空间取代了永久代。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于,元空间不在虚拟机设置的内存中,而是使用本地内存。
根据 Java 虚拟机规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
小结:
-
线程共享
-
存储已被虚拟机加载的类信息、常量、静态常量、编译后的代码
-
存在内存溢出 ,1.8以前会导致永久代内存溢出,1.8以后会导致元空间内存溢出
常量池
定义:可以看作是一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等。
一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池(包含:数量值、字符串值、类引用、字段引用、方法引用),这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。
运行时常量池
运行时常量池相对于 Class 文件常量池的另一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译器才能产生,也就是并非置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
小结:
具备动态性
-
可以使用String类的intern()方法将新的常量放入池中
-
常量池:就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法
名、参数类型、字面量信息 -
运行时常量池:常量池是.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
反编译class文件查看常量池
在控制台输入 javap -v 类的绝对路径 ,然后能在控制台看到反编译以后类的信息了
javap -v C:\Users\Administrator\Desktop\Practice\pachong\Test\target\classes\com\u
ser\Demo01.class
类的基本信息:
Classfile /C:/Users/Administrator/Desktop/Practice/pachong/Test/target/classes/com/user/Demo01.class
Last modified 2021-12-21; size 539 bytes
MD5 checksum b65bd01fa7519f59669b74170637c276
Compiled from "Demo01.java"
public class com.user.Demo01
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
常量池:
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/user/Demo01
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/user/Demo01;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Demo01.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/user/Demo01
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
虚拟机中执行编译的方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return