什么是JVM?一文简谈运行机制及基本原理!
JVM的基础概念
JVM的中文名称叫Java虚拟机,它是由软件技术模拟出计算机运行的一个虚拟的计算机。
JVM也充当着一个翻译官的角色,我们编写出的Java程序,是不能够被操作系统所直接识别的,这时候JVM的作用就体现出来了,它负责把我们的程序翻译给系统“听”,告诉它我们的程序需要做什么操作。
我们都知道Java的程序需要经过编译后,产生.Class文件,JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。
JVM的生命周期
JVM在Java程序开始执行的时候,它才运行,程序结束的时它就停止。
一个Java程序会开启一个JVM进程,如果一台机器上运行三个程序,那么就会有三个运行中的JVM进程。
JVM中的线程分为两种:守护线程和普通线程
守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。
普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。
权限足够的话,可以调用exit()方法终止程序。
JVM的结构体系
JVM的启动过程
1、JVM的装入环境和配置
在学习这个之前,我们需要了解一件事情,就是JDK和JRE的区别。
JDK是面向开发人员使用的SDK,它提供了Java的开发环境和运行环境,JDK中包含了JRE。
JRE是Java的运行环境,是面向所有Java程序的使用者,包括开发者。
JRE = 运行环境 = JVM。
如果安装了JDK,会发现电脑中有两套JRE,一套位于/Java/jre.../下,一套位于/Java/jdk.../jre下。那么问题来了,一台机器上有两套以上JRE,谁来决定运行那一套呢?这个任务就落到java.exe身上,java.exe的任务就是找到合适的JRE来运行java程序。
java.exe按照以下的顺序来选择JRE:
自己目录下有没有JRE
父目录下有没有JRE
- 查询注册表: HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\"当前JRE版本号"\JavaHome
这几步的主要核心是为了找到JVM的绝对路径。
jvm.cfg的路径为:JRE路径\lib\"CPU架构"\jvm.fig
jvm.cfg的内容大致如下:
- -client KNOWN?
- -server KNOWN?
- -hotspot ALIASED_TO -client?
- -classic WARN?
- -native ERROR?
- -green ERROR
KNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示给别的JVM去一个别名
WARN 表示不存在时找一个替代 、ERROR 表示不存在抛出异常
2、装载JVM
通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。
LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。
3、初始化JVM,获得本地调用接口
调用InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。
4、运行Java程序
JVM运行Java程序的方式有两种:jar包 与 Class
运行jar 的时候,Java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。
运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。
Class文件
Class文件由Java编译器生成,我们创建的.Java文件在经过编译器后,会变成.Class的文件,这样才能被JVM所识别并运行。
类加载子系统
类加载子系统也可以称之为类加载器,JVM默认提供三个类加载器:
1、BootStrap ClassLoader?:称之为启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等。
2、Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
3、App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。
除了Java默认提供的三个ClassLoader(加载器)之外,我们还可以根据自身需要自定义ClassLoader,自定义ClassLoader必须继承java.lang.ClassLoader 类。除了BootStrap ClassLoader 之外的另外两个默认加载器都是继承自java.lang.ClassLoader 。BootStrap ClassLoader 不是一个普通的Java类,它底层由C++编写,已嵌入到了JVM的内核当中,当JVM启动后,BootStrap ClassLoader 也随之启动,负责加载完核心类库后,并构造Extension ClassLoader 和App ClassLoader 类加载器。
类加载器子系统不仅仅负责定位并加载类文件,它还严格按照以下步骤做了很多事情:
1、加载:寻找并导入Class文件的二进制信息
2、连接:进行验证、准备和解析? ?
- 验证:确保导入类型的正确性? ?
- 准备:为类型分配内存并初始化为默认值? ?
- 解析:将字符引用解析为直接引用
3、初始化:调用Java代码,初始化类变量为指定初始值
方法区(Method Area)
在JVM中,类型信息和类静态变量都保存在方法区中,类型信息是由类加载器在类加载的过程中从类文件中提取出来的信息。
需要注意的一点是,常量池也存放于方法区中。
程序中所有的线程共享一个方法区,所以访问方法区的信息必须确保线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许去加载这个类,另一个必须等待。
在程序运行时,方法区的大小是可以改变的,程序在运行时可以扩展。
方法区也可以被垃圾回收,但条件非常严苛,必须在该类没有任何引用的情况下
类型信息包括什么?
1、类型的全名(The fully qualified name of the type)
2、类型的父类型全名(除非没有父类型,或者父类型是java.lang.Object)(The fully qualified name of the typeís direct superclass)
3、该类型是一个类还是接口(class or an interface)(Whether or not the type is a class )
4、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)
5、所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)
6、类型的字段信息(Field information)
7、类型的方法信息(Method information)
8、所有静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)
9、一个指向类加载器的引用(A reference to class ClassLoader)
10、一个指向Class类的引用(A reference to class Class)
11、基本类型的常量池(The constant pool for the type)
方法列表(Method Tables)
为了更高效的访问所有保存在方法区中的数据,在方法区中,除了保存上边的这些类型信息之外,还有一个为了加快存取速度而设计的数据结构:方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例方法的引用,保存那些父类中调用的方法。
Java堆(JVM堆、Heap)
当Java创建一个类的实例对象或者数组时,都在堆中为新的对象分配内存。
虚拟机中只有一个堆,程序中所有的线程都共享它。
堆占用的内存空间是最多的。
堆的存取类型为管道类型,先进先出。
在程序运行中,可以动态的分配堆的内存大小。
堆的内存资源回收是交给JVM GC进行管理的,
Java栈(JVM栈、Stack)
在Java栈中只保存基础数据类型和自定义对象的引用,注意只是对象的引用而不是对象本身哦,对象是保存在堆区中的。
拓展知识:像String、Integer、Byte、Short、Long、Character、Boolean这六个属于包装类型,它们是存放于堆中的。
栈的存取类型为类似于水杯,先进后出。
栈内的数据在超出其作用域后,会被自动释放掉,它不由JVM GC管理。
每一个线程都包含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。
每个线程都会建立一个操作栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法的每次调用,每个栈帧包含了三部分:
局部变量区(方法内基本类型变量、变量对象指针)
操作数栈区(存放方法执行过程中产生的中间结果)
运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)
写在最后:
欢迎大家关注我新开通的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。
觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!