1 什么是JVM
对于JVM(Java虚拟机)是什么,网上有很多官方的说法。简单来说,JAVA代码能够一次编写,到处运行的主要原因还是因为JVM。它屏蔽了和操作系统相关的信息,是JAVA程序只需要在JVM上生成JVM能够执行的字节码指令即可,JVM会把这些字节码解释为操作系统能够执行的机器指令。
2 JDK、JRE和JVM三者关系
**JDK(Java Development Kit)** : 提供给Java程序开发者操作使用的开发工具包。
**JRE(JavaRuntimeEnvironment,Java运行环境)**,Java程序运行的平台,运行Java程序的地方
**JVM(JavaVirtualMachine,Java虚拟机)**是JRE的一部分。虚拟的计算机,让Java程序和操作系统隔离,做到平台无关性。
3 Java代码编译
一个.java文件通过java源码编译器编译成.class文件。编译过程如下:
.java文件 -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器
-> 注解抽象语法树 -> 字节码生成器 -> .class文件。
生成的文件主要包含以下信息:
- 结构信息:包括class文件格式、版本号、各部分的数量与大小的信息
- 元数据:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池
- 方法信息:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。
4 类加载机制
4.1装载(Load)
查找和导入class文件
- ClassLoader通过一个类的全限定名获取定义此类的二进制流
- 将这个字节流所代表的的静态存储结构转化为方法区运行时的数据结构
- 在堆中生成代表这个类的所代表的的class对象,作为对方法区数据访问的入口
4.2链接(Link)
4.2.1验证(Verify)
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
4.2.2准备(Prepare)
为静态变量分配内存,并为其初始化默认值
4.2.3解析(Resovle)
把类中的符号引用转换为直接引用
符号引用:class文件中例如super_class,interface_class,fields_count等。
直接引用:直接指向目标的内存地址,偏移量。
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];
}
4.3 初始化(Initialize)
对类的静态变量,静态代码块执行初始化操作
5 类加载器
5.1分类
- Bootstrap ClassLoader加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
- Extension ClassLoader加载Java平台中扩展功能的一些Jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
- App ClassLoaderj加载classpath下指定的jar包和java.class.path 所指定目录下的类和jar包。
- Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
5.2 双亲委派机制
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载,递归操作,直到最上级。如果上级类没有加载,自己才会去加载。
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个类是否被加载过了
Class c = findLoadedClass(name);
// c==null 说明未被加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 是否有父类加载器,有则让父类的加载,递归操作
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
作用:1、防止重复加载一个.class。
2、保证核心的.class文件不被篡改。
6 JVM的运行时数据区
在装载的2,3步时,有提到堆,方法区等名词。现在着重介绍下运行数据区的布局。
官网介绍:[https://docs.oracle.com/javase/specs/jvms/se8/html/index.html](https://docs.oracle.com/javase/specs/jvms/se8/html/index.html)
The Java Virtual Machine defines various run-time data areas that are usedduring execution of a program. Some of these data areas are created on JavaVirtual Machine start-up and are destroyed only when the Java Virtual Machineexits. Other data areas are per thread. Per-thread data areas are created when athread is created and destroyed when the thread exits.
6.1 Method Area(方法区)
方法区是各个线程共享的内存区域,在虚拟机启动的时候创建。
它用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
注:方法区在JDK8中就是Metaspace,在JDK6和7时就是Perm Spacel。
运行时常量池在方法区中被分配,用于存放啊编译时期生成的字面量和符号引用
6.2 Heap(堆)
也是线程共享,虚拟机启动时创建
Java对象实例和数组都在其上分配
6.3 Java Virtual Machine Stacks(虚拟机栈)
线程私有,保存一个线程中方法的调用状态
每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
6.4 The pc Register(程序计数器)
一个JVM进程中有多个线程执行,而线程的执行权取决于CPU,当线程A执行到某个地方的时候,失去了CPU的执行权,切换到线程B,然后当线程A再次获得CPU的执行权时,怎么才能往刚才的地方执行呢?这就需要在线程中维护一个变量,记录当前线程的执行位置
程序计数器就是为了解决线程间切换如何恢复到正确的执行位置,每个线程都有一个独立的程序计数器(线程私有)
6.5 Native Method Stacks(本地方法栈)
当前线程执行的本地方法
public native int getInt(Object var1, long var2);
public native void putInt(Object var1, long var2, int var4);