1.JVM相关概念
官网地址:Java Platform Standard Edition 8 Documentation (oracle.com)
jvm: java虚拟机,是java程序的运行环境(java二进制字节码的运行环境)
jre:jvm+基础类库(java.lang包下工具类、IO、集合类库、线程类等)组成了完整的运行环境
jdk:是由jvm+jre+编译工具(javac、javap等)组成,它是开发人员开发、测试时的一个工具包。
2.HotSpot JVM架构
hotspot目前是最常用的jvm架构,可以通过java -version查看,其架构图如图所示:
由图中可以看出JVM结构主要分为三大模块:
a.class Loader SubSystem 类加载
b Runtime Data Areas 运行时数据区 又包含 方法区、堆栈、java虚拟机栈、程序计数器、本地方法栈
c.Execution Engine 执行引擎 又包含JIT 即时编译器 、垃圾回收器
2.1 Class File
程序员写的程序都是.java文件,而JVM是加载class文件的,那么第一个问题,如何将java文件转换为jvm加载的class文件呢?使用jdk自带的编译工具javac就可以将.java文件编译为.class文件,javac的编译过程主要是整合编译原理、C语言、java语言进行一系列的词法分析、语法树生成、字节码生成等等
比如我们将一段java代码编译成class文件后,class文件的内容都是16进制表示的字节码
该字节码对应的内容信息可参考官网:Chapter 4. The class File Format (oracle.com)
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
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字节 也就是对应上面16进制class文件中的前8位(2个为1个字节,1个字节为8bit,1位16进制数为4bit)
magic对应cafebabe。带着magic在官网上搜索。可以看到这么一句话: The magic item supplies the magic number identifying the class file format。代表的是这是一个class文件的标记
按上述方法,依次查找,就可以手动解析出整个class文件对应的含义。
当然,也可以采用jdk自带的反汇编命令javap对class文件进行反编译,就可以直接查看字节码信息和指令等信息
javap ‐v ‐c ‐p User.class > User.txt 进行反编译,查看字节码信息和指令等信息
3. 类加载机制
从第2节我们了解到了class文件的生成以及如何解读,那么有了class文件,CPU想要运行我们的程序,我们首先就需要把class文件以二进制的形式加载到内存中的一片物理地址中,那么JVM首先要做的就是类加载。类加载又分为3部分,Loading装载、Linking链接、Initialization初始化
官网:Chapter 5. Loading, Linking, and Initializing (oracle.com)
3.1 Loading
Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation。
说人话就是:装载就是把类和接口名以二进制的形式表示出来
加载的顺序是由顶向下:主要分为几部分 a.jre自带的一些基础jar包的加载(bootStrap加载器负责);b.java平台扩展的一些jar包;c.java应用中指定的jar包,可以理解为就咱们自己写的一些类;d.自定义的class。
问题: 假如说bootStrap已经加载了java.lang.String。而我们App ClassLoader也用到了App ClassLoader那么这个类如果被加载了两遍,就会出现new对象时找不到引用哪个的问题。因此,双亲委派机制就诞生了。双亲委派机制主要就是为了检查某个类是否已经加载了。
自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个Classloader已加 载,就视为已加载此类,保证此类只所有ClassLoader加载一次。
3.2 Linking
a. 首先保证被加载类的正确性
b.为类或接口的静态变量分配内存,并将其初始化为默认值(比如 private static int a = 10 会先将a初始化为0)
c. 将运行时常量池中的符号引用(指的是16进制的class文件中的常量池的一些16进制表示)转换为直接引用(物理内存中的一段地址)
3.3 Initialization
对类的静态变量,静态代码块执行初始化操作(a = 10)
4.运行时数据区
JVM通过类加载器将类信息,静态变量等信息加载到了内存中,那么java真正运行的时候还有方法、局部变量等信息,这些信息就会存储在运行时数据区中。运行时数据区可以以两个维度来划分进程生命周期(线程共享 全局变量)和线程生命周期 (局部变量)
方法区和堆栈都属于是进程生命周期,所有线程共享;java虚拟机栈、程序计数器、本地方法栈是线程生命周期(也就是线程安全的不存在并发问题)
官网地址:Chapter 2. The Structure of the Java Virtual Machine (oracle.com)
4.1Method Area(方法区)
方法区是各个线程共享的内存区域,在虚拟机启动时创建,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码(这个可以理解为代码中经常被使用的Util工具类)等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
4.2 Heap(堆)
a.java堆是java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享
b.Java对象实例以及数组都在堆上分配
c.堆内存空间不足时,也会抛出OOM
JVM为了提高性能和内存开销,在堆中还分配了一分内存空间存储字符串,也就是字符串常量池,下图举例说明了字符串的存储方式。
代码验证:
public class SCPDemo {
public static void main(String[] args) {
String str1="Jack"; // 这个常量一定会放到字符串常量池中
String str2="Jack";
String str3=new String("Jack");
String str4=str3.intern(); // 找字符串常量池中是否有该常量,如果有就直接返回,如果没有再创建
// equals 只会比较值 == 会比较地址
System.out.println(str1.equals(str2)); // true
System.out.println(str1==str2); // true
System.out.println(str1.equals(str3)); // true
System.out.println(str1==str3); // false
System.out.println(str1.equals(str4)); // true
System.out.println(str1==str4); // true
}
}
4.2.1 java对象内存布局
一个对象在内存中包括3个部分 对象头、实例数据和对齐填充。举个例子,假设我们有一个Student类,那么该类在内存中占多大内存呢?
class Student{
private double salary;
private Object obj;
}
实例对象占用8+8 = 16 对象头 8+8+4 20 对齐填充为8字节的整数倍 因此该对象占用40字节的内存空间
4.2.2 方法区引用指向堆
4.2.3 堆指向方法区
4.3 Java Virtual Machine Stacks(Java虚拟机栈)
a. 虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
b.每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
代码示例:
void a(){
}
void b(){
}
void c(){
}
流程图:
每一个栈帧中的局面变量表如果有实例对象引用,则指向堆内存
4.4 本地方法栈
4.5 The PC Register
如果线程正在执行java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Native方法,则这个计数器为空。
5.总结
一个java程序的执行,首先经历编译阶段由.java文件编译为16进制class文件并存放在磁盘或IO设备中,然后JVM可以适用于各个操作系统,将class文件编译为CPU可以运行的二进制文件。JVM首先会经过类加载器,将类名接口名、静态变量、常量等进行初始化并加载到内存中用二进制表示,存储在运行时数据区的方法区中,java代码中的实例对象,全局变量,字符串常量等存储在堆栈中,线程共享;java虚拟机栈为java中的具体线程执行,会以栈结构存储每一个方法,每一个方法都是用栈帧来表示,栈帧中包含了局部变量、操作数栈、动态链接和调用完成,局部变量若有实例引用则指向堆栈,动态链接为指向本地方法栈;本地方法栈则主要指向native方法;程序计数器记录的则是正在执行java方法的虚拟机字节码的地址,若是native方法,则计数器为空。