一图了解JVM

本文主要是对一个Java源文件从加载到运行有一个整体的了解,提供一个俯瞰的视角,各个点不会太详细深入。

下图是整理的大概路径:
在这里插入图片描述

大致过程:
.java源文件首先会由javac命令进行编译,产生.class字节码文件,然后经过类加载进入jvm。

这里列出JVM虚拟机官方文档:https://docs.oracle.com/javase/specs/index.html
注意,我们平时用的比较多的JDK1.8,是Oracle实现的HotSpot虚拟机,是JVM的一种。

为了更好的了解JVM相关内容,我们先准备一个Java文件如下,后期对其进行剖析:
在这里插入图片描述
从上面路径图看,词法分析、语义分析属于编译器范畴,以后有机会再深入学习。下面先简单看下class文件相关。

一、class字节码文件

class文件结构见JVM官方文档第四章4.1 The ClassFile Structure:

ClassFile {
    u4             magic; // 魔数,固定为0xCAFEBABE,用于标识Java文件
    u2             minor_version; // class的副版本号
    u2             major_version; // class的主版本号
    u2             constant_pool_count; // 常量池个数
    cp_info        constant_pool[constant_pool_count-1]; // 常量池,索引从1开始
    u2             access_flags; // 访问标志,如public、private等信息
    u2             this_class; // 类索引
    u2             super_class; // 父类索引
    u2             interfaces_count; // 接口计数器
    u2             interfaces[interfaces_count]; // 接口表
    u2             fields_count; // 字段计数器
    field_info     fields[fields_count]; // 字段表
    u2             methods_count; // 方法计数器
    method_info    methods[methods_count]; // 方法表
    u2             attributes_count; // 属性计数器
    attribute_info attributes[attributes_count]; // 属性表
}

记录几个点:
u4 代表占4个字节,内容为无符号整数。

关于constant_pool常量池:
常量池主要存放两大类常量:字面量(literal)和符号引用。字面量比较接近java语言层面的常量概念,比如文本字符串、声明的final的常量值等。符号引用属于编译原理方面概念。包括下面三类常量:类和接口的全局限定名。字段的名称和描述符。方法的名称和描述符。

关于fields[fields_count] methods[methods_count],存的分别是类的属性和方法。

使用JDK自带的反编译命令 javap -v Hello.class 可以查看class信息。这里不做详述,有兴趣可以深入研究。

class文件准备好后,就需要将其加载到JVM中,下面看下类加载阶段。

二、类加载阶段

JVM类加载主要分涉及以下几个阶段:

1.加载 Loading

ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象,该对象存在方法区。

2.链接 Linking

链接阶段可以分为三个小阶段:

  • 验证
    目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
  • 准备
    为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值。
    注意,这里是赋初始值,而非初始化。如int类型会赋值0,在初始化阶段再根据代码初始化赋值。这里还需要区分一下final修饰的变量,它们大部分是在编译期就决定了。
  • 解析
    将常量池的间接引用(符号引用)转换为直接引用(内存地址),解析包含字段解析、接口解析、方法解析。

3.初始化

执行Java代码,对成员变量进行赋值操作。

4.使用

加载完毕,正常使用。

5.卸载

三、JVM内存模型

在这里插入图片描述
上图就是JVM的简单模型。由图可知,JVM大致分为以下几部分:

  • 方法区(元空间)
  • 程序计数器
  • 本地方法栈

由于JVM是多线程模型的,我们从多线程的角度来解析:
首先,一个Java程序开启一个JVM进程,一个JVM进程占了一块内存。Java程序可以使用多线程,也就是说,JVM可以同时存在多个线程。那么,在这块JVM所占的内存中,必然存在线程私有部分,和线程共享部分。

线程共享部分:

  • 方法区(元空间)
    属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。就是class信息。
    JDK8(HotSpot)之后,方法区被移出虚拟机,在内存开辟一块空间,称之为元空间,作用类似方法区。解决之前永久代(方法区)常出现的OOM问题。


  • 堆是存储时的单位,对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。可以位于物理上不连续的空间,但是逻辑上要连续。
    JDK8之后,存储字符串信息。
    OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

线程私有部分:

  • 程序计数器
    记录当前程序运行位置。
    内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。

  • 本地方法栈
    虚拟机使用到的native方法

  • 栈(虚拟机栈)
    栈是运行时的单位,即Java 虚拟机栈,线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
    当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。

下面结合前面Hello.java进行解析:
在这里插入图片描述
首先经过类加载,”hello” “Java” 存储在了堆中字符串常量池。

我们知道,程序从main方法开始执行,JVM会首先开辟一个虚拟机栈,然后在栈中创建一个栈帧,用于存放main方法相关数据。

在该栈帧中,运行第一行代码 Hello hello = new Hello(); 虚拟机底层做法是,会在堆中开辟一块内存用来创建Hello对象,其中属性str,指向字符串常量池中的”Java”。由于属性msg是static,它是与class其它信息一起存在方法区(元空间)中的,这也就是常说的,static变量是属于类的。

执行 int age = 3;这里会在栈帧中开辟一块int内存,值为3。

接下来执行 hello.test(age);这里执行test方法,会在栈中开辟第二个栈帧,用于存放test方法相关数据。注意这里传了一个age进来,这个时候,会在该栈帧中创建一块int内存,值为3。

test方法往下执行,age =3;将该栈帧中的age值改为2。这里将age输出,我们可以看到结果是输出2。

test方法执行完毕,出栈销毁,继续返回main方法往下执行。
在这里插入图片描述
main方法中输出age为3。因为main栈帧中的age没有改变,传到test中去的,只是age的值,与test方法中的age为两个独立存在,互不影响。

最后一句输出hello。

到这里,我们应该对Java文件的编译加载运行有了一定的了解,具体细节上,后面会根据兴趣爱好继续深入查阅相关资料。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值