作者:Christy-1221 | 来源:互联网 | 2023-07-14 19:24
简介jvm是一个应用程序,其作用是运行jvm字节码,为什么叫jvm字节码呢?因为jvm支持运行各种语言编译成的字节码,而不仅仅是java,当然java字节码是最广泛的。其功能主要分
简介
jvm是一个应用程序,其作用是运行jvm字节码,为什么叫jvm字节码呢?因为jvm支持运行各种语言编译成的字节码,而不仅仅是java,当然java字节码是最广泛的。其功能主要分为两点:
- write once,run anywhere:在不同的硬件平台都有对应的jvm程序,字节码可以运行在任意平台的jvm中。
- 提供内存管理和垃圾回收等功能
JVM支持的指令流是基于栈的指令集架构,下面是将该方法所在类编译成的class文件,通过javap进行反编译后的代码,可以看到基本指令就是iconst,istore,iload等,另一种指令集架构是基于寄存器的指令集架构,代表就是汇编语言。来说说这两者的区别以及为什么JVM采用的基于栈的指令集架构。
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
基于栈式架构
- 设计和实现简单,适用于资源受限的系统
- 指令集中的指令基本是零地址指令,指令集更小,编译器容易实现
- 不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构
- 依赖硬件,可移植性差
- 性能优秀,执行高效
- 花费更少的指令完成一项操作,以1,2,3地址指令 为主。
为了更好的跨平台,Java的指令都是根据栈来设计
发展历程
Sun Classic VM : Java1.0发布,世界上第一款商用Java虚拟机,只提供解释器
Exact VM : jdk1.2时发布,提供了准确式内存管理,热点探测及编译器与解释器混合工作模式,短暂使用,很快就被HotSpot替换
HotSpot:jdk1.3时发布,目前JDK6,8等新版本默认虚拟机都是HotSpot,HotSpot含义就是热点代码探测技术。
JRockit:BEA公司发明,已被oracle收购,专注于服务器端应用,不包含解释器,是世界上最快的JVM。JRockit Real Time(毫秒或微妙级响应)&MissionControl(极低开销来监控,管理和分析生产环境的工具)是其特点。
J9:IBM发布,仅广泛应用于IBM的各种Java产品,移植性较差。
KVM,CDC/CLDC HotSpot:Oracle应用在移动领域的Java虚拟机,已几乎不用;Azul VM:应用在Azul Systems的专有硬件Vega系统上。Liquid VM:运用在BEA公司Hypervisor系统,实现了操作系统的必要功能,可直接与硬件交互,目前已停止。Apache Harmony:IBM和Intel联合开发,但Sun公司不让其获得JCP认证,最终退役。最后被应用在了Android SDK中。MicroSoft JVM:微软用以支持浏览器中的Java程序,只能在windows平台运行,但遭Sun公司指控,被迫抹掉。目前windows上都是HotSpot。
Taobao JVM:阿里基于OpenJDK,深度定制且开源的高性能服务器版Java虚拟机。GCIH技术实现了off-heap:将生命周期长的Java对象从heap中移到了heap之外,且GC不管理此类对象,降低回收频率和提高回收效率,heap之外的对象还能在多个Java虚拟机进程中实现共享。降低JNI开销…
类加载子系统(Classload sub system)
类加载子系统负责从文件系统或者网络中加载Class文件,加载的类信息会存放在方法区的内存空间,除了类的信息外,方法区中还存放运行时常量信息,可能还包括字符串字面量和数字常量。分为三步:加载,链接,初始化。
加载(Loading)
简单来说是指查找字节流,并据此创建类的过程。详细:通过类的全限定名(路径和文件名)获取此类的二进制流,将二进制字节流的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个java.lang.Class对象,作为这个类的数据的访问入口。
上述过程通过**类加载器(ClassLoader)**完成。类加载器分为启动类加载器(bootstrap),扩展类加载器(extention),应用类加载器(application)和自定义加载器。
启动类加载:由C++实现,在Java中用null来指代,只加载最基础最重要的类,例如JRE的lib目录下jar包中的类以及有虚拟机参数-Xbootclasspath指定的类。
扩展类加载器:其父类是启动类加载器,加载相对次要但通用的类,例如JRE的lib/ext目录下jar包的类,以及java.ext.dirs指定的类。(java 9 之后,改名为平台类加载器,加载更多类)
应用类加载器:其父类是扩展类加载器,加载应用程序路径下的类。
自定义加载器:实现特殊的加载方式,例如对class文件加密,加载时利用自定义的类加载器进行解密后加载。
双亲委派模型
JVM对class文件采用按需加载的方式,加载class文件时采用的双亲委派模型,如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WB59LCZR-1609912271538)(C:\Users\z672916\Desktop\日常文档\pic\类加载.webp)]
当收到类加载请求,会先查找是否加载过,若加载过,直接返回,反之不会自己先去加载,而是把这个请求委托给父类加载器,若父类加载器还存在父类加载器,进一步向上委托,最终可能会到顶层的启动类加载器进行加载。若父类加载器可以完成类加载,则成功返回,若无法完成,则子加载器会尝试加载。看下源码就很清晰了。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
优点:假设攻击者想篡改系统级别的类,例如String.class,但是呢启动类加载器已经加载了String.class对象,便不会再加载篡改后的,保证了一定的安全性;避免重复加载,父加载器加载过的class对象,不会再次加载。
PS:在JVM虚拟机中,类的唯一性由类加载器实例和类的全名一同确定,哪怕相同的class文件,由不同的类加载器加载,也会得到两个不同的类。
链接(LINKING)
链接分为三个步骤,分别是验证,准备,解析
验证(Verify)
确保class文件的字节流中包含的信息符合虚拟机要求,保证被加载类的正确性,不会危害虚拟机。包含四种验证:文件格式验证(所有class文件的开头的都是cafe babe,叫做魔数),元数据验证,字节码验证,符号引用验证。
准备(prepare)
为类变量(以static修饰的变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
- 不会为实例变量分配初始化,而实例变量会随着对象分配在堆中
- 设置初始值,会设为默认的零值
为类静态变量分配内存并且设置该类变量的默认初始值,即零值。不包含用static final 修饰的常量,static final在编译时候就分配了。不会为实例变量分配初始化,类静态变量会分配方法区中,而实例变量会随着对象分配在堆中。
解析(Resolve)
将常量池(指的是栈帧中的常量池)内的符号引用转换为直接引用。
解析操作往往伴随JVM执行完初始化之后再执行
初始化(initialize)
执行()方法的过程。
1.此方法是javac编译器自动收集类中所有类变量的赋值操作和静态代码块中的语句合并而来。()方法中的指令按照语句在原文件中出现的顺序执行。不同于构造器方法()方法(类加载完成后,创建对象时候将调用的 ()方法来初始化对象)
public class Test {
static {
// 给变量赋值可以正常编译通过
i = 0;
// 这句编译器会提示"非法向前引用"
System.out.println(i);
}
static int i = 1;
}
2.若该类包含父类,JVM会保证再执行子类()方法前,先调用父类的()方法。
3.JVM会保证在多线程下一个类的()方法同步加锁。
总结
JVM是一个基于栈的指令集架构,运行字节码的应用程序,并且提供了内存管理,垃圾回收等功能。类加载子系统是JVM运行字节码的第一步,将编译后的class文件加载到内存中。其完成流程如下: