跨平台语言(write once, run anywhere)
前端编译器(javac)的主要任务就是负责将java语法规范的java代码转换为符合jvm规范的字节码文件。
javac编译四步骤:1. 词法解析、2. 语法解析、3. 语义解析、4. 生成字节码
类的成员变量赋值:1、默认初始化 2、显式初始化 /代码块初始化 3、构造器初始化 4、有了对象之后(对象.属性或对象.方法)对成员变量赋值。
虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存某个方法和字段的最终内存布局信息,(字节码不会保存方法在内存的地址信息,调用要等加载到内存时符号引用转变为直接引用),因此这些字段和方法不经过转换是无法被虚拟机使用的。当虚拟机运行时,需要从常量池中获取对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到集体内存中
Loading(加载)
将java字节码文件加载到内存机器中,并在内存中构建java类的原型 ——类模板对象(常量池、类方法、类字段),通过反射调用。
加载完成的操作:
查找并加载类的二进制数据,生成class文件的实例
- 通过类的全类名获取类的二进制数据流
- 解析类的二进制数据流为方法区的数据结构
- 创建java.lang.Class类的实例,表示该模型。作为方法区这个类的各种数据访问入口。
类模型位置:加载的类会在JVM中创建相应的类结构,JDK1.8之前永久代,之后是元空间。
Class实例的位置:类将.Class文件加载到元空间后,会在堆中创建一个java.lang.Class对象,用来封装类位于方法区的数据结构。
Linking(链接)
Verification(验证)
验证加载的字节码是否合法、合理并符合规范。
Perparaton(准备)
为类中静态变量分配内存,初始化默认值。(默认初始化)
注意
- 这里不包含基本数据类型的字段用 static final(常量)修饰的情况。因为final在编译阶段就已经分配了,准备阶就会显示赋值。
- 这里不会为实例变量分配初始化,类变量会分配到方法区中,实例变量会随着对象分配到java堆中。
Resolution(解析)
将类、接口、字段和方法的符号引用转换为直接引用。
initialization(初始化)
为类的静态变量赋值正确的初始值。(显示初始化)
到了初始化阶段,才真正开始执行java程序代码
初始化阶段的重要工作就是执行类的初始化方法:< clinit>()方法
- 它是由静态成员的赋值语句以及static语句块合并产生。
- 该方法只能由java编译器生成并由JVM调用,程序开发者无法自定义同名对象,更无法直接在java程序中调用该方法。
结论:
- 对于基本数据类型字段来说,使用static final 修饰,则显示复制通常在Linking的prepare环节。
- 对于String来说,使用static final 加上字面量赋值,则显式初始化在Linking的prepare环节进行。
线程安全
< clinit>方法是线程安全的,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 < clinit>方法,其他线程都需要阻塞等待,直到活动线程执行 < clinit>完毕。
一个类只会执行一次初始化,之后调用直接返回准备好的信息。
如果一个类的 < clinit>方法中有耗时很长的操作,就可能造成多个线程阻塞,导致死锁且很难发现。
类的初始化情况(需要执行代码就会初始化)
主动使用:调用初始化方法 < clinit>
1.当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
2.当调用类的静态方法时,即当使用了字节码invokestatic指令。
3.当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。 (对应访问变量、赋值变量操作)
4.当使用java.lang.reflect 包中的方法反射类的方法时。比如: Class . forName( " com. atguigu. java . Test")
5.当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
6.如果-一个接口定义了default方法, 那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
7.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
8.当初次调用MethodHandle 实例时,初始化该MethodHandle 指向的方法所在的类。 (涉及解析REF_ getStatic、 REF_ putStatic、 REF invokeStatic方法句柄对应的类)
针对5,补充说明:
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
在初始化-一个类时,并不会先初始化它所实现的接口
在初始化- - 个接口时,并不会先初始化它的父接口
因此,-一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才
会导致该接口的初始化。
针对7,说明:
JVM启动的时候通过引导类加载器加载- 一个初始类。这个类在调用public static void main(String[])方法之前被链
接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。
被动使用
被动使用:不调用初始化方法
1.当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。当通过子类引用父类的静态变量,不会导致子类初始化
2.通过数组定义类引用,不会触发此类的初始化
3.引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
4.调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
类的卸载
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期、
及上图栈中的三个指针不再指向堆中的对象。
启动类加载器、系统类加载器、扩展类加载器几乎不可能被卸载。
自定义加载器被卸载的可能性稍大点。
允许回收满足以下三点:
- 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等。
- 该类对应的java. lang. Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
类加载器
类加载器是JVM执行类加载机制的前提,影响Loading。
ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个目标类对应的java.lang.Class对象是实例。然后交给JVM虚拟机进行链接、初始化等操作。
类的唯一性:这个类本身和加载这个类的类加载器共同确定,比较两个类是否相等,只有在两个类是由同一个类加载器加载的前提才有意义。
Class.forName():静态方法,返回一个Class对象,该方法将Class文件加载到内存时,会执行初始化。
ClassLoader.loadClass():实例方法,需要一个ClassLoader来调用此方法,该方法将Class文件加载到内存时并不会执行初始化,直到第一次使用才开始初始化。
双亲委派机制
优势:
- 避免类的重复加载,确保一个类的全局唯一性。
- 保护程序安全,防止核心API被随意篡改。
缺点: - 应用类加载器访问bootstrap类可以,反之可能出问题。(可以啃老,不能啃幼)
代码支持:
双亲委派机制在java. lang. ClassLoader . loadClass(String, boolean)接口中体现。该接口的逻辑如下:
(1)先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
(2)判断当前加载器的父加载器是否为空,如果不为空,则调用parent . loadClass(name, false)接 口进行加载。
(3)反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNu1l(name)接口,让引导类加载器进行
加载。
(4)如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用
java . lang. ClassLoader接口的defineClass系列的native接口加载目标Java类。
双亲委派的模型就隐藏在这第2和第3步中。
破环双亲委派机制:
- 重写loadClass()方法,直接破环双亲委派机制。
- bootstrap加载接口,接口的具体实现由ApplicationClassLoader加载,启动类加载器无法使用接口的实现方法。只能通过线程上下文类加载器通过对应用程序加载器的赋值进行使用,这样破坏了双亲委派机制的原理。
- 用户对程序动态性最求导致(模块热部署,代码热替换)
沙箱安全机制
将java代码限定在虚拟机特定运行范围中,并且严格限制代码对本地系统资源的访问。