大纲:
JVM
一、编译机制
二、类加载机制(装载、验证、准备、解析、初始化、使用、卸载)
三、类执行机制
源码编译机制:
(由.java源文件转为.class二进制字节码文件的过程)
使用命令 javac test.java 就可以编译test.java文件。生成test.class文件。
编译的过程:
词法分析、语法分析、语义分析、生成字节码
详细的过程:
源代码文件*.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> JVM字节码文件*.class
———————————————————————————————————————————
类加载机制:
在Class文件中描述的各种信息,最终都需要加载到虚拟机中才能运行和使用。那么虚拟机是如何加载这些Class文件的呢?
JVM把描述类数据的字节码.Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载/装载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。
一、加载
a.(类的加载指的是将类的.class文件中的二进制数据读入到内存中,生成对应的class对象)我们可以利用类加载器,实现类的动态加载。
b.在Java中,采用双亲委派机制来实现类的加载。委托模式。每个 ClassLoader 都有一个父加载器。类加载器在加载类之前会先递归的去尝试使用父加载器加载。父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类的时候才试图从自己的类路径加载,从而保证只有一个类进行加载,虚拟机有一个内建的启动类加载器(bootstrap ClassLoader),该加载器没有父加载器,但是可以作为其他加载器的父加载器。
c.委派机制则保证了基类都由相同的类加载器加载,这样就避免了同一个字节码文件被多次加载生成不同的 Class 对象的问题。
d.类加载器其实也是Java类。有四大类:
根加载器Bootstrap Class Loader:其负责加载Java的核心类,比如String、System这些类
扩展加载器Extension Class Loader:其负责加载JRE的拓展类库
系统应用加载器APP Class Loader:其负责加载CLASSPATH环境变量所指定的JAR包和类路径
用户自定义加载器Customer Class Loader
e.加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所ClassLoader加载一次。然后开始加载类,加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
f.java程序运行的场所是内存,当在命令行下执行:java HelloWorld命令的时候,JVM会将HelloWorld.class加载到内存中,并形成一个Class的对象HelloWorld.class。
g.类加载的方式:
命令行启动应用时候由JVM初始化加载
通过Class.forName()方法动态加载
通过ClassLoader.loadClass()方法动态加载
h.双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
二、链接
当类被加载后,系统会为之生成一个Class对象,接着将会进入连接阶段,链接阶段负责把类的二进制数据合并到JRE中
三个阶段
验证:检验被加载的类是否有正确的内部结构,并和其他类协调一致
准备:负责为类的类(静态)变量分配内存。并设置默认初始值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
解析:将类的二进制数据中的符号引用转换成直接引用
三、初始化
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
假如这个类还没有被加载和连接,则程序先加载并连接该类
假如该类的直接父类还没有被初始化,则先初始化其直接父类
假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机
创建类实例。也就是new的方式
调用某个类的类方法
访问某个类或接口的类变量,或为该类变量赋值
使用反射方式强制创建某个类或接口对应的java.lang.Class对象
初始化某个类的子类,则其父类也会被初始化
直接使用java.exe命令来运行某个主类
类实例创建过程
按照父子继承关系进行初始化,首先执行父类的初始化块部分,然后是父类的构造方法;再执行本类继承的子类的初始化块,最后是子类的构造方法
注意⚠️:
JAVA类首次装入时,会对静态成员变量或方法进行一次初始化,但方法不被调用是不会执行的, 静态成员变量和静态初始化块级别相同,非静态成员变量和非静态初始化块级别相同。(链接的准备阶段)
初始化阶段:
先初始化父类的静态代码--->初始化子类的静态代码-->
初始化父类的非静态代码--->初始化父类构造函数--->
初始化子类非静态代码—>初始化子类构造函数
分析:静态代码块在类加载的时候执行,而非静态代码快在生成对象时才被执行
类执行机制:
JVM是基于栈结构的体系结构来执行class字节码的,不同于windows和Linux基于寄存器结构。类的执行机制,主要是在Java栈上面完成。当一个线程被创建后,Java栈和PC寄存器就会被创建。Java栈由栈帧组成,调用一个方法,就会生成一个栈帧(可以理解为表示调用一个方法)。栈帧又由局部变量表、操作数栈和常量池引用组成。
其他:
常量:
一、用final修饰的成员变量表示常量,值一旦给定就无法改变!一般都用大写字符为常量赋值。在常量中,往往通过下划线来分隔不同的字符。对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
二、final关键字与static关键字同时使用
由于Java是面向对象的语言,所以在Java常量定义的时候还有与其它编程语言不同的地方。如一段程序代码从编辑到最后执行,即使需要经过两个过程,分别为代码的装载与对象的建立。不同的过程对于常量的影响是不同的。
1).不使用static修饰情况:
例如:final long CURRENT_TIME=System.currentTimeMillis();
默认情况下,定义的常量是在对象建立的时候被初始化。如果在建立常量时,直接赋一个固定的值,而不是通过其他对象或者函数来赋值,那么这个常量的值就是恒定不变的,即在多个对象中值也使相同的。但是如果在给常量赋值的时候,采用的是一些函数或者对象(如生成随机数的Random对象),那么每次建立对象时其给常量的初始化值就有可能不同。可见,使用final的Java常量定义并不是恒定不变的。
2).使用static修饰情况:
例如:static final long CURRENT_TIME=System.currentTimeMillis();
这个是一个静态的概念。即当利用这个关键字来修饰一个变量的时候,在创建对象之前就会为这个变量在内存中创建一个存储空间。以后创建对对象如果需要用到这个静态变量,那么就会共享这一个变量的存储空间。也就是说,在创建对象的时候,如果用到这个变量,那么系统不会为其再分配一个存储空间,而只是将这个内存存储空间的地址赋值给他。如此做的好处就是可以让多个对象采用相同的初始变量。当需要改变多个对象中变量值的时候,只需要改变一次即可。从这个特性上来说,其跟常量的作用比较类似。不过其并不能够取代常量的作用。
变量
类变量:static int allClicks=0;
局部变量:类的方法中的变量
public void method(){
int i =0; //局部变量
}
实例变量: String str="hello world";