前言:
该章节知识点梳理:本文主要是入门和了解jvm,不做深入
1.Java代码是如何运行起来的?
2.类加载到使用的过程?
3.验证准备和初始化的过程?
4.类从加载到使用核心阶段:初始化、类加载器的概念、类加载的层级结构、什么是JVM的内存区域划分?自定义类加载器如何实现?
5.JVM中有哪些内存区域?
6.字节码执行引擎、程序计数器?
7.Java虚拟机栈、Java堆内存?
8.tomcat的类加载器体系?【这里放在3.6位置】
1.Java代码是如何跑起来的?
凡是只要接触过Java的基本上都知道Hello World入门程序,一个Hello World程序是如何运行起来的呢?
【看个简单的模型图】
user.java首先需要打包/编译成user.class字节码文件,如果使用java -jar的命令启动,实际上就会启动一个JVM进程,JVM就会来负责运行这些.class字节码文件,也就是相当于负责运行我们写好的系统,所以平时我们写好的某个系统在一台机器上部署启动的时候,其实也就是启动了一个jvm,由jvm来负责运行这台机器上运行的这个系统,jvm要运行这些.class字节码文件中的代码,首先需要将.class文件中包含的各种类给加载到JVM中,就会有一个【类加载器的概念】,将.class字节码文件加载到jvm中,供后续代码运行使用,接着最后一步,jvm就会基于自己的字节码执行引擎来执行加载到内存中写好的这些类了,例如User类,例如代码中有一个main()方法,那么jvm就会从这个main()方法开始执行里面的代码,当还需要哪个类的时候,例如new classes();就会继续使用类加载器来加载对应的类;
我不看!我不看!不看不知道,一看一激灵,嗦嘎死累!
2.类加载的过程:JVM的类加载机制到底是怎么样的?
2.1:jvm什么时候加载类呢?
首先了解一下什么是快乐星球?对,就是快乐,只有快乐的时候才能到达快乐星球,答案就知晓了,只有当使用到这个类的时候,jvm就会加载这个类。
2.2:简单认知类从加载到使用
加载-->验证-->准备-->解析-->初始化-->使用-->卸载
1.如何加载以及什么时候加载已经在上文提过了
2.验证:简单说就是根据Java虚拟机规范,校验加载进来的.class文件中的内容是否符合制定的规范,假如.class文件的字节码压根不符合规范,那么jvm是没有办法去执行这个字节码文件的;
3.准备阶段:准备阶段就是,写好的一些类中都有一些类变量,假设有一个Classes类,他的class文件内容被加载到内存之后,会进行验证,确认这个字节码文件的内容是规范的,再进行准备工作【准备工作其实就是给classes类分配一定的内存空间(然后给类中的类变量也就是static修饰的变量)分配内存空间】,来设置一个默认的初始值。注意:这里的默认初始值并不是赋值!
4.解析阶段:这个阶段实际上就是【将符号引用替换为直接引用】的过程,相对来说比较复杂,涉及到JVM的底层原理;
5.初始化:什么是类初始化的代码呢?
public static int age = User.getAge();
在初始化阶段就会执行类的初始化代码,比如上面的User.getAge();代码就会在这个阶段执行,完成配置项的读取,然后赋值给int age;
5.1:什么时候会初始化一个类呢?
一般来说,比如new User();来实例化类的对象了,此时就会触发类的加载到初始化的全过程,把这个类准备好,然后实例化一个对象出来,或者是包含main()方法的主类,必须是立马初始化的;【PS:如果初始化一个类的时候,发现他的父类还没有初始化,就必须要初始化他的父类】;
3.双亲委派模型类加载,也就是上图中JVM加载的一个过程,详细如下:
3.1:先了解Java中的类加载器:
1.启动类加载器:Bootstrap ClassLoader;
2.扩展类加载器:Extension ClassLoader;
3.应用程序类加载器:Application ClassLoader;
4.自定义类加载器;
3.2:双亲委派机制:(也就是孙子找爸爸找爷爷,爷爷找儿子找孙子):
jvm的类加载器是有亲子层级机构的,也就是说启动类加载器在最上层的(爷爷),扩展类记载器在第二层(爸爸),第三层是应用程序类加载器(孙子),最后一层才是自定义类加载器(重孙)【架构如图】
3.3:自定义类加载器如何实现?
自己写一个类,继承ClassLoader类,重写类加载的方法,然后在代码里面可以用自己的类加载器去针对某个路径下的类加载到内存里来;
3.4:双亲委派模型类加载的UserDemo故事:
比如JVM现在要加载我们系统中的User类,此时应用程序加载器会问自己的爸爸(扩展类加载器)是否能加载到这个类,扩展类问自己的爸爸启动类加载器,能不能加载到这个类,启动类在Java安装目录下没有找到这个类,然后就说:自己找去!!接着就下推给扩展类,也没有找到,然后就下派给应用程序类加载器,一下发现就在这里,于是就将这个类加载到内存中了。
3.5:双亲委派模型类加载small总:
所谓的“双亲委派模型”:就是先找父亲去加载,不行的话再由儿子来加载,这样可以避免多层级的加载器结构重复加载某些类;
3.6:tomcat的类加载器体系:【唯一的变化就是违背了双亲委派机制】
解译:
tomcat自定义了common,catalina,shared等类加载器,其实就是用来加载tomcat自己的一些核心基础类库的,tomcat为每个部署在web应用都有一个对应的webapp类加载器,负责加载部署的web应用的类,至于jsp类加载器,就是给每个jsp都准备了一个jsp类加载器,tomcat是打破了双亲委派机制的
每个webapp负责加载自己对应的那个web应用的class文件,也就是写好的某个系统打包好的war包中的所有class文件,不会传导给上层类加载器去加载。
4.什么是JVM的内存区域划分?
4.1:概念:
jvm在运行写好的代码时,jvm必须使用多块内存空间的,不同的内存空间用来存放不同的数据,然后配合写的代码流程,才能让系统运行起来;
4.2:举例:例如我们现在知道JVM会加载类到内存中来提供后续的运行,那么这些类加载到内存后,放到哪里去了呢?
代码运行起来时,是不是需要执行类中写的一个个的方法呢?运行方法的时候,方法中有很多变量之类的东西,是不是需要放在某个内存区域中?代码中创建一些对象,这些对象是否需要内存空间来存放?【看图】
这就是为什么JVM中必须划分出来不同的内存区域,就是为了写好的代码在运行的过程中根据需求来使用的
5.JVM中有哪些内存区域?
1.存放类的方法区:
1.1:JDK1.8之前:这个方法区在JDK1.8之前的版本中,代表JVM中的一块区域,主要是放从于".class"文件中加载进来的类,还会有一些类似常量池的东西放在这个区域中;
1.2:JDK1.8之后:JDK1.8之后,这块区域的名称改了,叫做“Metaspace”
,可以认为是"元数据空间"的意思,主要还是存放写的各种类的信息;
2.执行代码指令用的程序计数器[字节码执行引擎]:
第一:写好的Java代码会被翻译成字节码对应各种字节码指令,Java代码通过JVM跑起来的第一件事情就明确了,首先Java代码会被编译成字节码指令,然后字节码指令一定会被一条条的执行,从而实现写好的代码效果,所有JVM加载类信息到内存之后,实际上就会使用自己的字节码执行引擎,去执行写的代码编译出来的代码指令【这也就是为什么代码是从上到下执行的原理了】;
第二:执行字节码指令的时候JVM中就需要一个特殊的内存区域了,那就是"程序计数器";这个程序计数器就是用来**《记录当前执行的字节码指令的位置的》**,也就是记录目前执行到了哪一条字节码指令。
第三:JVM支持多个线程的,所以其实写好的代码可能会开启多个线程并发执行不同的代码,所以就会有多个线程来并发的执行不同的指令代码《因此每个线程都会有自己的程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了》
6.Java虚拟机栈
6.1:首先要知道Java代码在执行的时候,一定是线程来执行某个方法中的代码;
6.2:demo:例如一个最简单的helloworld程序,也会有一个main线程来执行main()方法的代码,在main线程执行main()方法的代码指令的时候,就会通过main线程对应的程序计数器记录自己指令的位置;
6.3:当然在方法中会定义一些方法内的局部变量,因此JVM必须有一块区域是来保存每个方法的局部变量等数据的,这个区域就是Java虚拟机栈;
6.4:每个线程都有自己的Java虚拟机栈
例如:比如这里的main线程就会有自己的一个Java虚拟机栈,用来存放自己执行的那些方法的局部变量,如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧,栈帧里就有这个方法的局部变量表,操作数栈,动态链接,方法出口等等东西,暂时主要关注局部变量,main()线程执行了main()方法,那就会给main()方法创建一个栈帧,压入main线程的Java虚拟机栈,同时在main()方法的栈帧里,会存放对应的“例如age”局部变量【如图】
示例code:
public class Study {
public static void main(String[] args) {
loadMe();
}
public static void loadMe(){
Boolean flag = false;
}
public static void oneMe(){
Integer study = 1;
}
}
在main线程中的对象方法中又定义了一个局部变量:“flag”,那么main线程在执行上面的loadMe方法的时候,就会为loadMe方法创建一个栈帧压入线程自己的Java虚拟机栈里面去,然后在栈帧的局部变量表中就有"flag"这个局部变量了。假设还有一个方法叫做oneMe(),也有一个局部变量:“study”;
其他的陆续的方法和局部变量进入Java虚拟机栈,当oneMe()方法执行完毕了,就会把oneMe()方法对应的栈帧从Java虚拟机栈给出栈。然后LoadMe()方法也执行完毕了,也会把LoadMe()方法从Java虚拟机中出栈;以上差不多就是一个Java虚拟机的一个概念性的机制;
7.Java堆内存
main线程执行main()方法的时候,会有自己的程序计数器,此外还会依次将main()方法,LoadMe方法,oneMe()方法的栈帧压入Java虚拟机栈,存放每个方法的局部变量。JVM中非常关键的区域:Java堆内存,也就是存放在代码中创建的各种对象;
User user = new User();
new User();这个代码就是创建了一个User类的对象实例,这个对象实例中包含一些数据,如:
private String username;
private Integer age;
那么这个User类中的username就是属于这个对象实例的一个数据,类是User这样的对象实例,就会存放在Java堆内存中,Java堆内存区域会放入类似User的对象,因为在main方法中创建了User对象的,那么在线程执行main方法代码的时候就会在main方法对应的栈帧的局部变量表中,**让一个引用类型的User局部变量来存放User对象的地址信息。**相当于可以认为Java虚拟机栈中的局部变量表中的“User”指向了Java堆内存中的User对象;
文章评论