JVM内存模型以及类加载过程分析
写在前面
JVM内存模型可以说是面试中常客了,足见其重要性,本文就一起来看下,以求下次遇到这样的面试题可以把面试官按在地上摩擦,摩擦,我的滑板鞋,霍霍!!!
1:JVM内存模型
先来看一下JVM内存模型的图:
其中线程共享的区域有方法区,堆。线程独享的区域有程序计数器,虚拟机栈,本地方法栈。我们按照线程独享还是共享来分别看下。
1.1:线程独享
1.1.1:程序计数器
用来存放线程执行字节码位置的区域,因为只是存储一个位置,所以是一块很小的内存空间。也是唯一的一块不会发生内存溢出的区域。
1.1.2:虚拟机栈
线程执行方法时分配线程栈的区域,每个线程一个线程栈,每次方法的调用都会生成对应的栈帧,并压入线程栈中,如下图:
每个栈帧中包含了执行方法所必须的内容,主要是局部变量表,操作数栈,异常表,等,如下图:
通过参数-Xss来设置虚拟机栈的大小。
1.1.3:本地方法栈
同虚拟机栈,不过是用来执行native方法的,底层通过C语言编写。
1.2:线程共享
1.2.1:堆
堆是JVM中最大的一块内存区域,通过-Xms<size>,-Xmx<size>来设置堆内存的最小值和最大值,一般最好二者设置为一致,这样可以避免扩容时申请内存对程序的影响。堆分为年轻代和年老代,其中年轻代又分为Eden和survivor,survivor又分为from和to,相关主要参数如下:
-Xms<size>:设置堆内存最小值
-Xmx<size>:设置堆内存最大值
-Xmn<size>:设置年轻代大小
-XX:NewRatio=<ratio>:设置年老代和年轻代比例,优先级低于-Xmn。
例如-XX:NewRatio=3表示老年代:年轻代 = 3:1,年轻代占堆的1/4。如果设置了-Xmn,则此参数失效。适合通过比例调整,不关心精确大小。
-XX:SurvivorRatio=<ratio>:eden和单个survivor的比例
例如-XX:SurvivorRatio=4表示Eden:From = 4:1,Eden占年轻代的4/6。Survivor过小可能导致对象过早晋升,过大则浪费空间。
以1.8为例看下结构图注意图中的元空间不属于堆,属于本地内存:
1.2.2:方法区
这里方法区是一个抽象的概念,是JVM规范中的一部分内容,永久代和元空间都是其具体的实现,永久代是1.8之前对于方法区的实现,而元空间是1.8版本对于方法区的实现,用来替换永久代。方法区主要用来存储类的元信息,如类成员变量,类方法信息,运行时常量池等。
2:类加载到执行过程分析
2.1:类加载
为了更加清晰的展示类加载整个过程,我们通过一个实际的类在不同的加载过程中对于jvm内存模型的影响来看下,如下类:
package com.demo.xx;
public class JVMCase {
// 常量
public final static String MAN_SEX_TYPE = "man";
// 静态变量
public static String WOMAN_SEX_TYPE = "woman";
public static void main(String[] args) {
Student stu = new Student();
stu.setName("nick");
stu.setSexType(MAN_SEX_TYPE);
stu.setAge(20);
JVMCase jvmcase = new JVMCase();
// 调用静态方法
print(stu);
// 调用非静态方法
jvmcase.sayHello(stu);
}
// 常规静态方法
public static void print(Student stu) {
System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge());
}
// 非静态方法
public void sayHello(Student stu) {
System.out.println(stu.getName() + "say: hello");
}
}
class Student{
String name;
String sexType;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSexType() {
return sexType;
}
public void setSexType(String sexType) {
this.sexType = sexType;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
假定执行了程序new JVMCase(),正式开始加载过程,首先执行第一个阶段加载,会通过类加载器app classload将字节码加载到方法区中,这没什么好说的。
接着看,连接阶段,连接阶段又分为验证,准备,解析,首先是验证阶段,验证文件的合法性,即格式是否符合JVM的要求等。然后是准备阶段,准备阶段会为静态变量申请内存并赋予初始值,对于常量public final static String MAN_SEX_TYPE = "man";会直接赋值man,对于静态变量public static String WOMAN_SEX_TYPE = "woman";会赋予String的默认值null,如下图:
接着是解析,解析阶段是将符号引用(如字面量com.xx.Stu#sayHi)解析为实际引用(com.xx.Stu#sayHi对应的可以直接使用的指针地址)。到这里连接阶段就结束了,进入初始化阶段,该阶段会执行<clinit>方法,该方法是在编译阶段,编译器收集类中的静态变量,静态代码块动态生成的方法,本例中<clinit>方法如下仅示意:
function clinit() {
WOMAN_SEX_TYPE = "woman";
}
初始化结束后,WOMAN_SEX_TYPE就会被赋值为woman了,如下图:
2.2:main方法执行
执行main,首先会为main线程在虚拟机栈中分配一个线程栈,然后创建main方法的栈帧,压入线程栈,开始执行main方法,执行代码Student stu = new Student();后就会在堆中创建stu对象,在栈帧的局部变量表中引用堆中的对象,如下:
同样的执行JVMCase jvmcase = new JVMCase();会在堆中创建对应的对象,接着执行实例方法jvmcase.sayHello(stu);和静态方法print(stu);,此时如下图:
图示,可能不是百分百的正确,知道大概的意思就行,有时候不用抠的那么细。
我觉得,知道核心的流程就可以了,如果真的需要,再详细的了解细节也不迟!!!
写在后面
参考文章列表
多知道一点
<clinit>方法
这是在编译阶段,编译器通过收集静态变量,静态代码块,而构成的一个方法,代码的顺序是程序的自然顺序,会在初始化阶段来执行,另外如果有父类的话会先执行父类的<clinit>方法,如下类:
package com.demo.xx;
public class Father {
static {
fatherStatic = 0;
System.out.println("father 静态代码块");
}
private static int fatherStatic = 1;
public static void main(String[] args) {
}
}
package com.demo.xx;
public class Son extends Father {
private static int sonStatic = 1;
static {
sonStatic = 0;
System.out.println("son 静态代码块");
}
public static void main(String[] args) {
System.out.println("sonStatic最终值是:" + sonStatic);
}
}
运行:
father 静态代码块
son 静态代码块
sonStatic最终值是:0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)