首先通过Javac命令将.java
文件编译生成.class
字节码文件。
Javac是Java编译命令,编译过程分为四步。
.class
文件加载到JVM中经过一系列类加载流程,由解释器解释执行和JIT即时编译器将字节码文件编译成本地机器码执行。
字节码必须通过类加载机制加载到JVM后方能执行,执行有三种模式,解释执行、JIT编译执行、JIT编译和解释器混合执行(主流JVM默认执行的方式)。混合模式优势在于解释器在启动时先解释执行,节省编译时间。
解释执行: 来一行代码,解释一行,大部分不常用的代码,采用此种方式
即时编译: 对于部分热点代码,虚拟机将该部分字节码编译生成机器指令,以提高Java虚拟机的运行效率
CPU调度线程执行本地机器码
Class文件
本质上是一个以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。JVM根据其特定的规则解析该二进制数据,从而得到相关信息。Class文件
采用一种伪结构来存储数据,它有两种类型:无符号数和表。
Class文件的结构属性:
public、private
,类型是否被标记为final
public、private、protected
)、是否是静态变量(static
)、可变性(final
)、数据类型(基本数据类型、对象、数组)等Class 文件
中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件
加载到内存,并对数据进行加载、验证、解析和初始化
,最终形成可以被虚拟机直接使用的 数据类型,这个过程称为虚拟机的类加载过程。
与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。
一个类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段开始的先后顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 语言的动态绑定。
加载:查找并加载类的二进制数据
在加载阶段,虚拟机需要完成以下三个步骤:
java.lang.Class
对象,作为对方法区中这些数据的访问入口。验证: 确保被加载类的正确性
确保Class文件
的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证分为4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用
-Xverifynone参数
来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值零值的阶段,这些内存都将在方法区中分配。
注: 此时分配的是类变量(static
),不包括实例变量。初始化的值是数据类型的默认零值,比如0、0L、null、false
等。
解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
在Java中对类变量进行初始值设定有两种方式:
JVM初始化步骤:
启动类加载器:
Bootstrap ClassLoader
,负责加载存放在JDK的安装目录下的jre\lib
中,或被-Xbootclasspath参数
指定的路径中的,并且能被虚拟机识别的类库(如rt.jar
,所有的java.*
开头的类均被Bootstrap ClassLoader
加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:
Extension ClassLoader
,该加载器由sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK安装目录下的jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的所有类库(如javax.*
开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:
Application ClassLoader
,该类加载器由sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)
所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()
方法动态加载
3、通过ClassLoader.loadClass()
方法动态加载
Class.forName()和ClassLoader.loadClass()区别?
Class.forName():
将类的.class文件加载到JVM中,还会对类进行解释,执行类中的static块
;ClassLoader.loadClass():
只会将.class文件
加载到JVM中,不会执行static
中的内容,只有在newInstance()
方法创建类对象时才会去执行static块
。Class.forName(name, initialize, loader)
带参函数也可控制是否加载static
块。并且只有调用了newInstance()
方法构造函数创建类的对象时才会加载static块
。当AppClassLoader
加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader
去完成。
当ExtClassLoader
加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader
去完成。
如果BootStrapClassLoader
加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
若ExtClassLoader
也加载失败,则会使用AppClassLoader
来加载,如果AppClassLoader
也加载失败,则会报出异常ClassNotFoundException
。
双亲委派机制的好处:
String类
与JDK中的String类
会优先使用JDK中的系统API)为什么要破坏双亲委派?
举个: 我们常用数据库驱动Driver
接口,Driver
定义在JDK
中,但其实现是各个数据库服务商,比如:MySQL
的MYSQL CONNECROR
,因此DriverManger
要加载各个Driver
接口实现类进行管理,但是DriverManager
是由启动类加载器进行加载的,而这个启动类加载器默认值加载JDK安装目录下面的lib文件下的类库,但我们真正要加载的是各个实现类,需要有应用程序类加载器进行加载,这个时候就需要启动类加载器委托应用程序类加载器去加载Driver
实现类,从而破坏了双亲委派。
破坏方式:
loadclass
方法。双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所以如果改写他的加载规则,相当于打破双亲委派机制。JNDI服务(JDBC/JCE/JAXB/JBI)
是Java的标准服务,它的代码是由启动类加载器进行加载的,但是JNDI的作用就是进行资源的集中管理和查找,它需要调用由(服务厂商提供的实现类)开发人员在classpath
下的类代码,但是启动类加载器不会进行加载。java.lang.Thread
类的setContextClassLoader()
方法进行设置。如果创建线程时还未设置,它会从父线程继承一个,如果在应用程序全局范围内没有设置,那么这个线程上下类加载器就是应用程序类加载器。参考文章: