一、JVM的基本介绍
JVM(Java Virtual Machine)又被分为三大子系统,类加载子系统,运行时数据区,执行引擎。
image.png
JVM是Java Virtual Machine的缩写,它是一个虚构出来的计算机,一种规范。
JVM类似于一台小电脑运行在操作系统环境上,它和操作系统交互,与硬件不直接交互。
Java文件是如何被运行的
比如我们现在写了一个HelloWorld.java,JVM并不认识它,所以它需要被编译,让它成为一个能被JVM读懂的二进制文件HelloWorld.class
类加载器
如果JVM想要执行这个.class文件,我们需要将其装进一个类加载器中,类加载器会把所以的.class文件全部搬到JVM里面来。
image.png
方法区
方法区 是用于存放类似于元数据信息方法的数据的,比如类信息、常量、静态变量、编译后代码等
类加载器将.class文件搬过来就是先丢到这一块上
堆
堆 主要放了一些存储的数据,比如对象实例、数组等,它和方法区都同属于线程共享区域,也就是说他们都是线程不安全的
栈
栈 是代码运行空间,我们编写的每一个方法都会放到栈里面运行
我们会听说 本地方法栈 或 本地方法接口,它俩底层是使用C来进行工作的,和Java没有太大的关系。
程序计数器
主要就是完成一个加载工作,类似于一个指针,指向下一行我们需要执行的代码。和栈一一样,是线程独享的,也就是说每个线程都独有一块区域,不会存在并发和多线程的问题。
小总结
1.Java文件经过编译后变成.class字节码文件
2.字节码文件通过类加载器搬运到JVM虚拟机中
3.虚拟机主要的5大块:方法区、堆都是线程共享,的,由线程安全问题;栈和本地方法栈以及程序计数器都是独享区域,不存在线程安全问题,而JVM的调优是围绕堆,栈两大块进行
简单的例子说明
public class Student {
public String name;
public Student(String name){
this.name = name;
}
public void sayName(){
System.out.println("student's name is " + name);
}
}
public class App {
public static void main(String[] args) {
Student student = new Student("lili");
student.sayName();
}
}
执行main方法的步骤如下:
- 编译App.java后得到App.class,执行App.class, 系统会启动一个JVM进程,从classpath路径种找到一个名为App.class的二进制文件,将App的类信息加载到运行时数据区的方法区内,这个过程叫App类加载
- JVM找到App的主程序入口,执行main方法
- 这个main中的第一条语句为Student student = new Student("lili"),是让JVM创建一个Student对象,这个时候方法区里没有Student类的信息,所以JVM会马上加载Student类,把Student类的信息放到方法区中
- 加载完Student类后,JVM堆中为一个新的Student实例分配内存,然后调用构造函数初始化Student实例,这个Student实例持有指向方法区中的Student类的类型信息的引用
- 执行student.sayName()时,JVM根据student的引用找到student对象,然后根据student对象持有的引用定位到方法区中student类的类型信息的方法表,获取sayName()的字节码地址
- 执行sayName()
其实也不用管太多,只需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找。
二、类加载器的介绍
之前也提到了它是负责加载.class文件的,它们在文件开头会有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载,而是否能够运行则由 Execution Engine来决定
类加载器的流程
从类加载到虚拟机内存中开始,到释放总共有7个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三部分统称链接
加载
- 将class文件加载到内存
- 将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM时的数据结构)
- 在堆中生成一个代表这个类的 java.lang.Class对象作为数据访问的入口
链接
链接就是将Java类的二进制代码合并到java的运行状态中的过程。
- 验证:确保加载的类符合JVM规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的时间,其实就是一个安全检查
- 准备: 为static变量在方法区中分配内存空间,设置变量的初始值,例如: static int a = 3(注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量时对象初始化时赋值的)
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程符号引用,比如我现在import java.util.ArrayList这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
初始化
初始化其实就是一个赋值的操作,它会执行一个类构造器的<clinit>()方法。由编译器自动收集类中所以变量的赋值动作,此时准备阶段时的那个static int a = 3的例子,在这个时候旧正式赋值为3
卸载
GC将无用对象从内存中卸载
类加载器的加载顺序
加载一个class类的顺序也是优先级的,类加载器从最底层开始往上的顺序是这样的
- BootStrap ClassLoader:rt.jar(采用native code实现,是JVM的一部分,主要加载JVM自身工作需要的类,如java.lang.、java.uti.等; 这些类位于$JAVA_HOME/jre/lib/rt.jar)
- Extention ClassLoader:加载扩展的jar包(扩展的class loader,加载位于$JAVA_HOME/jre/lib/ext目录下的扩展jar)
- App ClassLoader: 指定的classpath下面的jar包(系统class loader,父类是ExtClassLoader,加载$CLASSPATH下的目录和jar;它负责加载应用程序主函数类。)
-
Custom ClassLoader:自定义的类加载器
image.png
双亲委派机制
当一个类收到了加载请求时,它时不会先自己去尝试加载的,而是委派给父类去完成,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的class)时,子类加载器才会自行尝试加载
这样做的好处是,加载位于rt.jar包中的类时不管时哪个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,这样保证了使用不同的类加载器得到的是同一结果。
其实这个也是一个隔离的作用,避免了我们的代码影响了JDK的代码,例如:
public class String{
}
这种时候,我们的代码肯定会报错,因为在加载的时候其实是找到了rt.jar中的String.class
原文:Java识堂 大白话带你认识JVM
网友评论