深化学习JVM-内存架构图(二)
JVM深化学习-内存架构图篇
本篇聚集于对JVM内存架构图的深度总结与解析。文中将逐一翔实介绍内存架构图中的各部分,并深化了解JVM运转机制与内存办理战略。
内存架构图
JVM架构图中包括了 类加载子体系(上篇JVM详细介绍了类加载体系)、运转时数据区、履行引擎、本地接口、本地办法库。
- 关于JVM内存模型,在jdk1.8时做了调整,将Method Area(办法区/永久代)转化为元空间并放在本地内存中。
- 类加载子体系担任将类的字节码加载至运转时数据区中的办法区中,并在堆内存中生成Class方针作为类信息的拜访进口。
办法区
办法区(Method Area)是Java虚拟机内存结构中的一个重要组成部分,它是线程同享的区域。这意味着多个线程能够一同拜访办法区中的信息。
办法区的首要作用是存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。它就像一个知识库,为后续Java程序的运转供给各种信息以保证程序能够运转。
- 类信息
- 类全限定名:完好类名(包括包名和类名),用于在JVM中表明仅有的标识一个类。如
com.example.MyClass
这个类,com.example
是包名,MyClass
是类名,这个全限定名能够协助 JVM 在很多类中精确地定位和区别不同的类。 - 字段信息:包括变量称号、类型、拜访修饰符。用于JVM能够了解类中包括那些字段。
- 办法信息:包括办法的称号、回来类型、参数列表(参数的类型和次序)以及办法的拜访修饰符等。JVM 经过这些信息来确认怎么调用办法以及办法的调用规矩。
- 接口调用:假如一个类完结了接口,办法区会存储接口的相关信息,包括接口的全限定名、接口中界说的办法签名等。这有助于 JVM 查看类是否正确地完结了接口以及在运转时完结接口相关的多态调用。
- 类全限定名:完好类名(包括包名和类名),用于在JVM中表明仅有的标识一个类。如
- 静态变量:类相关的静态变量是在类初始化阶段完结的。而且这些静态变量在整个程序的生命周期内都存储在办法区中,并能够被类的例实拜访。
- JIT优化代码:JIT(Just - In - Time)编译是一种优化技能,JVM 在运转进程中会对频频履行的热门代码(Hot - Spot Code)进行动态编译。这些被 JIT 编译后的代码会存储在办法区中。JIT 编译将字节码转化为机器码,进步了代码的履行功率。例如,关于一个频频履行的循环体或许常常调用的办法,JVM 或许会对其进行 JIT 编译,使得后续的履行能够直接运用编译后的机器码,而不是每次都进行字节码解说。
- 运转时常量池:是办法区的一部分,它是在类加载进程中由字节码文件中的常量池转化而来的。常量池中的信息包括字面量和符号引证,在类加载后,这些信息会被解析并存储到运转时常量池中。
- 字面量:字面量是在代码中直接呈现的常量值。例如,在
String str = "hello";
中,"hello"
便是一个字面量。在类加载进程中,字面量会被存储到运转时常量池中,并将其转化为在内存中的实践表明办法,通常是一个指向常量值的内存地址。这样,在程序运转进程中,当需求拜访这个字面量时,能够经过这个内存地址快速获取。 - 符号引证:符号引证是一种在编译阶段对类、办法、字段等的引证表明办法。例如,在代码中的一个办法调用
myMethod()
,在编译阶段这仅仅一个符号引证,它表明需求调用一个名为myMethod
的办法,但并没有确认这个办法的实践内存地址。在类加载进程中,符号引证会被解析并转化为直接引证,也便是确认办法的实践内存方位,这个进程也是在运转时常量池中完结的。这些直接引证信息会被存储在运转时常量池中,以便在程序运转时能够精确地调用相应的办法或拜访相应的字段。
- 字面量:字面量是在代码中直接呈现的常量值。例如,在
元空间代替永久代?
内存巨细:在jdk1.8之前,永久代是放在堆内存中的,也便是说在JVM内存中,可是跟着项目的杂乱度、结构运用、三方库的运用,永久代中需求存储的类越来越多,导致固定内存巨细的永久代无法适用,所以这儿把永久代转化成元空间,并把它放到本地内存中,不占用jvm内存空间。
废物收回:永久代的废物收回相对杂乱。由于它里边存储的类信息等数据的生命周期和一般方针不同,废物收回器很难精确判别哪些类信息是能够收回的。例如,一个类加载后,即便没有任何实例方针存在了,只需这个类还在被其他类引证(比方经过反射),它在永久代中的信息就不能被收回。这种杂乱的收回机制导致永久代的内存整理功率较低。
功能问题:永久代中的类数据和字符串常量池等内容混在一同,当进行废物收回或许内存整理时,会对整个永久代进行操作。永久代的废物收回会触发 Full GC,这是十分耗时的进程,在高负载体系中影响较大。而元空间独立于堆内存,大大减少了永久代相关的 Full GC 次数,因此在运转时减少了长期的中止。
办法区和其他内存结构的联系?
- 办法区与 Java 虚拟机栈(Java Virtual Machine Stack)也有相关。在办法调用进程中,Java 虚拟机栈中的栈帧会包括对办法区中办法信息的引证。例如,当一个办法被调用时,栈帧中的动态链接部分会依据办法的符号引证在办法区中查找并确认办法的实践履行版别,然后完结办法的正确调用。
- 办法区与堆区(Heap)联系:堆区是方针实例的存储之地。当履行
new
操作创立方针时,JVM 需求依据办法区中存储的类信息来构建方针。在废物收回进程中,办法区中的类信息对堆区方针的收回战略起到关键作用。堆区方针的可达性剖析(断定方针是否可被收回的重要进程)不只考虑方针之间的引证联系,还触及方针与办法区中类信息的相关。例如,当废物收回器在堆区收回方针时,它需求依据方针的类信息(如是否有finalize
办法等)来确认收回的办法和次序。
栈区
栈区是JVM内存结构中的一个重要组成部分,担任办理办法调用和履行时的数据存储。
在Java虚拟机中,栈与线程密切相关。每个线程在创立时都会分配一个JVM栈。这个栈用于存储办法调用时的相关信息,包括局部变量表、操作数栈、动态链接、办法回来地址。
JVM栈中的每个办法调用都会创立一个栈帧(Stack Frame),栈帧是栈中的根本存储单位,用于寄存办法调用时的信息。
下面我将以一段代码简述栈帧中存储的各个部分的意义:
package focus.total;
public class JVMStudy {
private static final int initData = 6;
public static User user = new User();
public int compute(){
int a=1;
int b=2;
int c = (a+b)*10;
return c;
}
public static void main(String[] args) {
JVMStudy jvmStudy = new JVMStudy();
jvmStudy.compute();
System.out.println("核算完结");
}
}
当上述程序被履行时,JVM虚拟时机分配一个Main主线程,并为该线程分配一个栈内存空间,并创立main栈帧,程序计数器初始化到main办法字节码指令的第一条,并依照次序履行,当履行到compute()办法时,创立compute办法的栈帧。在cmopute办法中我将对栈帧中的存储结构进行逐一介绍。
反汇编JVMStudy.class代码调查JVM履行的指令
下面关于字节码指令的解说其实就现已把局部变量表和操作数栈的意义解说清楚了。
- 字节码文件自身是 Java 源代码经过编译后生成的中心表明办法,它包括了一系列依照特定次序排列的指令码,这些指令码便是 Java 程序在 JVM 中运转的根底。
- 程序计数器的中心作用便是记载当时线程正在履行的字节码指令的地址。在上述反汇编得到的字节码履行次序指令码中,程序计数器会依据字节码的履行流程顺次指向对应的指令方位。
Compiled from "JVMStudy.java"
public class focus.total.JVMStudy {
public static focus.total.User user;
public focus.total.JVMStudy();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int compute();
Code:
//iconst_1 : 将int类型的1压入操作数栈中
//istore_1 : 将int类型的值存入局部变量表中
//两个组合起来其实便是咱们首先在咱们的操作数栈中存储int值1,然后在局部变量表中存入a,然后将操作数栈中的值取出,赋给局部变量表中的a ---- int a=1;
0: iconst_1
1: istore_1
//iconst_2 : 同上,区别是值为2
//istore_2 : 同上
// 这儿考虑一个问题,假如此刻发生了线程切换,那么当从头回来这个线程时,怎么知道从哪里持续履行?
//程序计数器:记载下一行行将运转的代码的内存地址。
// 程序计数器是每个程序在运转时都会给他分配的一段内存空间代码,寄存下一次行将运转的指令内存地址。
// 那么当咱们程序在履行3的这行指令时,来了一个优先级高的线程将cpu抢占曩昔,那么此刻该线程会履行完3指令之后,在程序计数器+1,然后让出CPU,并挂起。当抢占的线程运转结束之后,该线程从头拿到cpu运用权,此刻就会依照程序计数器中存储的方位区履行。
2: iconst_2
3: istore_2
// iload_1 : 将局部变量表中第1个方位的整数值加载到操作数栈顶
// iload_2 : 从局部变量2中装载int类型值
// iadd :履行int类型的加法
// 这三个指令其实便是 ,从局部变量表中别离取出a的值和b的值放入操作数栈,然后调用iadd指令,将两个操作数取出操作数栈并完结加法指令。把成果从头压回咱们的操作数栈。
// 此刻咱们的操作数栈中寄存了 int 值 3
4: iload_1
5: iload_2
6: iadd
// bipush :向操作数栈中放入 int 值 10
// imul : 乘法 3 * 10
7: bipush 10
9: imul
// istore_3 : 将栈顶的整数值存储到局部变量表的第3个方位。
// iload_3 : 将局部变量表中第3个方位的整数值加载到操作数栈顶。
10: istore_3
11: iload_3
12: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // 创立一个新的JVMStudy方针
3: dup // 仿制栈顶的JVMStudy方针引证
4: invokespecial #3 // 调用JVMStudy的结构办法 "<init>":()V
7: astore_1 // 将栈顶的JVMStudy方针引证存储到局部变量1
8: aload_1 // 将局部变量1中的JVMStudy方针引证加载到栈顶
9: invokevirtual #
4 // 调用JVMStudy方针的compute办法,回来一个int
12: pop // 弹出栈顶的int回来值(不运用)
13: getstatic #5 // 获取System类的out字段(PrintStream方针)
16: ldc #6 // 将字符串 "核算完结" 压入栈顶
18: invokevirtual #7 // 调用PrintStream的println办法,打印字符串
21: return // 从main办法回来
static {};
Code:
0: new #8 // 创立一个新的User方针
3: dup // 仿制栈顶的User方针引证
4: invokespecial #9 // 调用User的结构办法 "<init>":()V
7: putstatic #10 // 将栈顶的User方针引证存储到静态字段user
10: return // 从静态初始化块回来
动态链接:在 Java 虚拟机的运转机制中起着关键作用。在之前论述对办法区的了解时,就现已触及到栈与办法区之间存在的动态链接联系。咱们都知道,在类加载阶段的解析进程中,会完结符号引证到直接引证的转化,这一转化实践上便是将办法区中的常量池转变为运转时常量池的进程。而这儿所说的动态链接,其间心操作便是把办法的符号引证凭借动态链接这种办法,精确地链接到办法在内存中的实践地址,然后为办法的成功调用奠定根底,保证在程序运转进程中,当需求调用某个办法时,能够经过这种动态链接机制敏捷定位到办法的实践履行代码地点的内存方位并顺畅履行。
那么此刻就有一个疑问,在类加载阶段就现已完结转化了,为什么这儿还需求进行转化?
那是由于这儿的动态链接,首要用于处理在编译时无法确认详细调用方针类型的状况,特别是在多态(虚办法调用)下发挥作用。
多态场景下的动态链接示例(以Animal
为例)
-
假定存在一个
Animal
类,它有一个虚办法makeSound()
。有两个子类Dog
和Cat
,它们别离重写了makeSound()
办法。 -
当咱们在代码中有这样的句子:
Animal animal = new Dog(); animal.makeSound();
,在编译阶段,编译器看到的是经过Animal
类型的引证animal
调用makeSound()
办法,它生成的字节码中关于这个调用只要一个符号引证,这个符号引证指向Animal
类的makeSound()
办法。 -
可是在运转时,由于
animal
实践指向的是Dog
方针,动态链接就会发挥作用。它会依据方针头确认animal
指向的是Dog
类型,然后查找Dog
类的虚办法表,在虚办法表中找到Dog
类重写后的makeSound()
办法的实践内存地址(直接引证),最终履行Dog
类的makeSound()
办法。这便是动态链接在多态场景下将符号引证转化为直接引证的详细进程,保证了依据方针的实践类型调用正确的办法。
动态链接与其他概念的相关
- 动态链接与栈帧密切相关。在每个栈帧中,都有一个指向运转时常量池该栈帧所属办法的引证,这个引证用于支撑动态链接。当一个办法被调用时,会创立栈帧,栈帧中的动态链接部分参加到寻觅实践要调用的办法的进程中。
- 动态链接也和类加载进程彼此弥补。类加载进程中的解析阶段首要处理那些在编译期就能确认仅有调用版别的办法(如静态办法、私有办法等),将它们的符号引证转化为直接引证。而动态链接侧重于在运转时处理虚办法和接口办法等需求依据实践状况确认调用版别的办法的符号引证到直接引证的转化。
办法回来地址
就如下,咱们调用compute办法时,就会在办法回来地址中记载,用于存储compute办法回来后的地址。
public static void main(String[] args) {
JVMStudy jvmStudy = new JVMStudy();
jvmStudy.compute();
System.out.println("核算完结");
}
程序计数器
程序计数器是JVM内存模型中的一个重要部分,它是线程私有的,也便是说每个线程都会依照自己的程序计数器指向指令去按次序履行。
程序计数器的首要职责是告知JVM接下来应该履行哪条字节码指令。
在类加载阶段,程序计数器没有发挥作用。而当程序启动时,主线程的程序计数器会初始化为指向 Main 办法字节码指令的首条。随后,跟着字节码指令的逐渐履行,它持续更新,每履行完一条指令,便精准指向下一条指令地址,以此保证办法中的字节码指令依序履行。若遭受 if - else 分支句子,程序计数器会依据判别成果跳转至相应的字节码指令地址;当遇到办法调用时,它会先留存当时办法当时字节码指令的地址,然后跳转至被调用办法内持续履行,待被调用办法履行结束,再从头回到之前留存的方位,然后保证程序履行流程的连贯性与精确性。
堆区
堆区(Heap Area)是JVM内存模型中的一个重要部分,它是线程同享的,这意味着多个线程能够一同拜访该区域,获取方针、数组等相关信息。
堆区首要是用于存储Java方针实例(包括数组方针),在程序履行进程中,经过new关键字创立的方针都会在堆区种分配内存空间。
示例代码:
Person person = new Person();
person.setName("张三")
person.setAge(22);
当程序运转完这段代码后,就会在堆种存储person方针及name和age特点信息,在栈种存储Person类型的方针引证,该引证指向堆内存种实践的存储地址。
关于堆的内存模型:
在Java8中能够看到堆内存被划分为年青代和老时代
- 年青代被划分为三部分,Eden区和两个巨细严厉相同的Survivor区,依据JVM战略,在经过几回废物收回之后,任然存活鱼Survivor区的方针将会被移动到老时代中。
- 老时代:首要保存生命周期常的方针,一般是经历过屡次gc都没有被收回的方针。
- 年青代和老时代(1:2),其间年青代又分为 Eden 、s0 、s1(8:1:1)
简述方针在堆中的一个简略进程:
当程序new一个新的方针,就会把它放在堆中的Eden区,可是当Eden区域放满之后,就需求进行GC -- (minor gc)
这个gc是由履行引擎后台建议一个废物搜集线程,去堆Eden中的方针进行收回(可达性算法),在收回的进程中假如仍有方针被引证那么就将这些方针仿制到 幸存区(其间一个空的,这两者必定有一个或许两个都是空的) ,然后就这样gc收回,假如一个方针在经过 15 次废物收回后仍然存活于幸存区中,那么就会将这个方针放到老时代中。尔后GC -- (full gc)也会对老时代的废物进行收回。
一段代码调查堆内存溢出的状况
下面这段代码必定会内存溢出,由于咱们新new的方针都是寄存在lists调集中,而lists又是在main办法栈帧中的变量,是一个GC Root,所以这些新new的方针都不会被收回!!
public class HeapTest{
public static void main(String[] args){
ArrayList<HeapTest> lists = new ArrayList<>();
while(true){
lists.add(new HeapTest());
Thread.sleep(5);
}
}
}
运用jvisualvm进行检测
本地办法栈
在Java虚拟机(JVM)中,本地办法栈(Native Method Stack)是专门为本地办法(native methods)服务的内存区域。当一个线程调用本地办法时,会运用本地办法栈来履行这些办法。
当Java程序调用本地办法时,JVM会保存当时栈帧,然后在本地办法栈空间中创立当时本地办法的栈帧,经过JNI调用本地办法,本地办法履行结束之后,JVM回到之前的栈帧,持续履行Java代码。
简略一句话便是履行本地办法的。
示例:本地办法栈的运用
以下是一个简略的本地办法示例,展现了怎么运用本地办法栈:
public class NativeExample {
// 声明本地办法
public native void nativePrint();
static {
// 加载本地库
System.loadLibrary("NativeExample");
}
public static void main(String[] args) {
new NativeExample().nativePrint();
}
}
假定对应的C代码如下:
#include <jni.h>
#include <stdio.h>
#include "NativeExample.h"
// 完结本地办法
JNIEXPORT void JNICALL Java_NativeExample_nativePrint(JNIEnv *env, jobject obj) {
printf("Hello from native code!\n");
}
履行引擎
履行引擎中包括了解说器、JIT即时编译器、废物收回
解说器:解说器是履行引擎的一个重要组成部分,它的首要工作办法是逐行读取字节码指令并进行解说履行。例如,当遇到字节码指令中的iload
(将局部变量加载到操作数栈)时,解说器会依据指令的参数,从局部变量表中找到对应的变量并将其加载到操作数栈中,这个进程是一个一个指令顺次进行的。
JIT即时编译器:JIT 即时编译器是为了进步 Java 程序的履行功率而引进的。它会在程序运转进程中,对那些频频履行的热门代码(经过一些动态监测机制确认)进行编译。这个编译进程是将字节码转化为机器码,这样在后续履行这些代码时,就能够直接履行现已编译好的机器码,而不是每次都经过解说器解说字节码。
废物收回:这块首要是针对堆内存中的废物方针收回,避免跟着程序的运转方针越来越多导致OOM,详细的GC会在下一篇JVM深化学习中说到。