引入类加载
类加载:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
类预加载:
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
加载.class文件的方式
从本地系统中直接加载
通过网络下载.class文件
从zip,jar等归档文件中加载.class文件
从专有数据库中提取.class文件
将Java源文件动态编译为.class文件
类的生命周期
其中类加载的过程包括了加载、连接(验证、准备、解析)、初始化几个阶段。在这五个阶段中,加载、连接(验证、准备)和初始化这几个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
1. 加载:查找并加载类的二进制数据
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些存储结构数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
2 验证:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3 准备:为类的静态变量static分配内存,并将其初始化为默认值
准备阶段是正式为类变量(静态变量static)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化(new instance)时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器 (方法)
< clinit >()之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
这里还需要注意如下几点:
对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过,常量是在编译期就确定下来的。
准备阶段静态变量就被赋值为初始值,并放入常量池;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
·如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
如果类字段的字段属性表中存在Constant Value属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为Const Value属性所指定的值。
假设上面的类变量value被定义为常量,声明必须赋值: public static final int value = 3;
编译时Javac将会为value生成Constant Value属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
4 解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
5 初始化
初始化,为类的静态static变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量(静态变量)进行初始化。完成加载、验证、准备没有完成初始化称作半初始化。
在Java中对类变量进行初始值设定有两种方式:
①声明类变量时指定初始值 ②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接(验证 准备),则程序先加载并连接(验证 准备)该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
1.创建类的实例,也就是new的方式
2.访问某个类或接口的静态变量,或者对该静态变量赋值
3.调用类的静态方法
4.反射(如Class.forName(“com.shengsiyuan.Test”))
5.初始化某个类的子类,则其父类也会被初始化
6.Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
通过类名获取Class对象,不会触发类的初始化。如System.out.println(Person.class);
通过ClassLoader默认的loadClass方法,也不会触发初始化动作
结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
1.执行了System.exit()方法
2.程序正常执行结束
3.程序在执行过程中遇到了异常或错误而异常终止
4.由于操作系统出现错误而导致Java虚拟机进程终止
类加载器
package com.neo.classloader;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
运行后,输出结果:
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null
从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到,于是就返回null。
父类加载器并不是通过继承关系来实现的,而是采用组合实现的。
站在Java虚拟机的角度来讲,类加载器分为:
1.启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;
2.所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。 2)动态地创建符合用户特定需要的定制化构建类。 3)从特定的场所取得java
class,例如数据库中和网络中。
JVM类加载机制
1.全盘负责:一个类加载器加载该类和该类依赖的类
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
2.父类委托:先让父类加载器加载
先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
3.缓存机制:已经读取过的Class放在缓存中
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
类的加载
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
package com.neo.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行初始化块
// Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
// Class.forName("Test2", false, loader);
}
}
public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}
分别切换加载方式,会有不同的输出结果。
Class.forName()和ClassLoader.loadClass()区别
Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法才用调用构造函数,创建类的对象 。
双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把先请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
ClassLoader源码分析:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由**启动类加载器**加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果**父类加载器和启动类加载器**都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派模型意义:
系统类防止内存中出现多份同样的字节码 保证Java程序安全稳定运行
自定义类加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass方法即可。下面我们通过一个示例来演示自定义类加载器的流程
package com.neo.classloader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.neo.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:
1、这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。
2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
3、这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
类和实例的初始化顺序
类初始化过程
- 一个类要创建实例需要先加载并初始化该类
- main方法所在的类需要先加载和初始化
- 一个子类要初始化需要先初始化父类
- 一个类初始化就是执行 client()方法
- client()方法由静态类变量显式赋值代码和静态代码块组成
- 类变量显示赋值代码和静态代码块从上到下顺序执行
- client()方法只执行一次
实例初始化过程
- 实例初始化就是执行init()方法
- init()方法可能重载有多个,有几个构造器就有几个init方法
- init()方法由非静态实例变量显式赋值代码和非静态代码块、对应构造器代码组成
- 非静态实例变量显式赋值代码和非静态代码块从上到下顺序执行,而对应构造器的代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的init()方法
init()方法的首行是super()或super(实参列表),即对应父类的init()方法
类加载,验证、准备(静态变量赋值默认初始值)之后进行初始化。接下来执行初始化执行赋值语句(赋值语句a=5或执行函数 a=fun() ),初始化代码块,构造函数等等操作为初始化操作。
实例化一个对象(new 对象 执行init() )的过程分3步(第2和第三步可以发生指令重排序,这也是为什么单例模式双重校验必须加volatile的原因):
- 堆中分配内存 (实例变量的值是默认值 int a=0)
- 实例变量赋值 (int a=100)
- 栈内存变量引用指向这个实例
不加volatile拿到实例半初始化的值(0)直接使用。
// volatile在单例模式下也有应用
class VolatileSingleton {
private volatile static VolatileSingleton singleton;
private VolatileSingleton (){}
public static VolatileSingleton getSingleton() {
if (singleton == null) {
synchronized (VolatileSingleton.class) { // 细化锁,锁太粗就太耗性能
if (singleton == null) {// 获取锁后,如果不判断,就有可能别的线程new,后获取不到最新的new出来的对象
singleton = new Singleton();
}
}
}
return singleton;
}
}
初始化顺序
1.无继承关系
先执行静态变量(static变量)初始化语句(static初始化代码块:初始化语句和初始化代码块哪个在前先执行哪个)、再执行实例变量初始化语句(初始化代码块:初始化语句和初始化代码块哪个在前先执行哪个)、最后执行构造函数。总结下即先static,再实例,先赋值(代码块)【赋值和代码块哪个在前哪个先执行】,最后构造函数。(Springboot ioc的字段属性注入@Autowired在构造函数之后)
static变量初始化即由默认初始值到初始值,static变量赋值语句和static代码块哪个先出现,先执行哪个。实例变量由默认值到初始值,实例变量语句和初始代码块先出现哪个,先执行哪个
package com.example.ABReference;
public class Test04 {// 注释是顺序
private static Test04 t1 = new Test04();//第 1
private static int i1;
private static int i2 = 2; //第 3
static { //第 4
System.out.println("static块:i1没有赋值语句经过+1后变成1 i1="+i1);
System.out.println("static块:i2经过赋值语句=2后变成2 i2="+i2);
}
public Test04() {//第2 因初始化t1所以第2 ;构造函数第5 执行两次
i1++;
i2++;
System.out.println("第一次执行时为 t1赋初值 加1,i1由初始默认值1变成1;i2由初始默认值0变成1;第二次执行时执行构造方法又各自加1");
System.out.println("i1:"+i1+" i2:"+i2);
}
public static void main(String[] args) {
Test04 t2 = new Test04();
System.out.println("t2.i1 = " + t2.i1);
System.out.println("t2.i2 = " + t2.i2);
}
}
结果
第一次执行时为 t1赋初值 加1,i1由初始默认值1变成1;i2由初始默认值0变成1;第二次执行时执行构造方法又各自加1
i1:1 i2:1
static块:i1没有赋值语句经过+1后变成1 i1=1
static块:i2经过赋值语句=2后变成2 i2=2
第一次执行时为 t1赋初值 加1,i1由初始默认值1变成1;i2由初始默认值0变成1;第二次执行时执行构造方法又各自加1
i1:2 i2:3
t2.i1 = 2
t2.i2 = 3
package com.example.ABReference;
public class Test03 {
private int i1 = printCommon();//第 2
private static int i2 = printStatic();//第 1
public Test03() {//第 4
System.out.println("构造方法");
}
{// 代码块和赋值语句同级别,哪个在前,哪个先执行
System.out.println("代码块");//第 3
}
public static int printCommon() {
System.out.println("i1 is init!");
return 1;
}
public static int printStatic() {
System.out.println("i2 is init!");
return 2;
}
public static void main(String[] args) {
Test03 t = new Test03();
}
}
结果
i2 is init!
i1 is init!
代码块
构造方法
2. 有继承关系 。
总的来说先初始化父类后子类。先类变量(static)后 实例变量+构造函数
具体的来说,类的初始化(父类static => 子类static) => 实例的初始化(父类实例变量初始化 => 父类构造函数 => 子类实例变量初始化 =>子类构造函数)
多态特性: 父类构造函数执行时调用函数 时实际上调用了子类的函数
实例变量初始化包括非静态代码块初始化语句,两者同级,按代码先后顺序执行
static:包括静态变量初始化和静态块初始化语句,两者同级,按代码先后顺序执行
package com.example.ABReference;
class A {
private int i = 9;// 第8 i 9
protected static int j=9;//最先 1 j 9
static {//第二 2
System.out.println("-- Load First SuperClass of static block start!-- ");
System.out.println("j = " + j); // j 9 经历初始化
System.out.println("-- Load First SuperClass of static block End -- ");
}
public A() {// 第9
System.out.println("------- Load SuperClass of structor start --------");
System.out.println("Frist print j = " + j);// j 9
j = 10;// j 10
m();//第10 多态,执行子类
// 第11 继续执行下边
System.out.println("k = " + k);// k 11
System.out.println("Second print j = " + j); // j 10
System.out.println("----------- Load SuperClass End ----------- ");
}
private static int k = getInt();//第3 k 11
public static int getInt() {// 因3执行所以是 第4
System.out.println("Load SuperClass.getInt() k: "+k);// k 0
return 11;
}
static {// 第5
System.out.println("--- Load Second SuperClass of static block!-------");
System.out.println("j = " + j);// j 9
System.out.println("k = " + k); // k 11
System.out.println("-- Load Second SuperClass of static block End -- ");
}
public void m() {
System.out.println("SuperClass.m() , " + "j = " + j);
}
}
class B extends A {
private int a = 10;// 第12 a 11
private static int b = 12;// 第6 b 12
static { // 第7 b 12
System.out.println("---- Load SubClass of static block! b:"+b+"----");
System.out.println("-- Load SubClass of static block End b:"+b+"--");
}
public B() {// 第13
System.out.println("Load SubClass of structor");
m(); // 第14
// 第15
System.out.println("--- Load SubClass End ---- ");
}
public void m() {//第10 多态,执行子类 a 0(a没有赋初值,默认值0); 第14 a 10 因为位于第12之后 。执行两次
System.out.println("SubClass.m() ," + "a = " + a);
}
}
public class Test5 {
public static void main(String[] args) {
A a = new B();
}
}
结果
– Load First SuperClass of static block start!–
j = 9
– Load First SuperClass of static block End –
Load SuperClass.getInt() k: 0
— Load Second SuperClass of static block!-------
j = 9
k = 11
– Load Second SuperClass of static block End –
---- Load SubClass of static block! b:12----
– Load SubClass of static block End b:12–
------- Load SuperClass of structor start --------
Frist print j = 9
SubClass.m() ,a = 0
k = 11
Second print j = 10
----------- Load SuperClass End -----------
Load SubClass of structor
SubClass.m() ,a = 10
— Load SubClass End ----
构造函数保证先父类再子类 super
实际上父子类对象初始化过程,可以看成是先执行子类的构造器,但是因为在子类构造器中必须第一句是super(),所以先执行父类的构造器(执行父类构造器之前还要先进行父类实例变量的初始化。)—这也是为什么会出现多态现象:先执行父类构造器过程中,如果调用了子类重写的方法后,会调用子类方法。其实就是调用子类的构造器,跳转到了父类,但是this实例还是子类对象。那么执行顺序严格上其实应该是 子类构造器(执行super) =>父类实例变量初始化 =>父类构造器 =>子类实例变量初始化=>继续执行子类构造器
如果一个类不写构造器,那么系统会自动加上一个默认的无参构造器 ,而且在首行会有super()(其实是父子类无参构造器的调用,因为一个类最终继承Object类,最终会调用到Object类的构造器)。如果一个类加上有参构造方法,那么编译器不会自动加无参构造方法,也就是说只存在有参构造方法。
继承关系:super关键字
只能指代父类对象,指代父类的构造方法,只能放在首行,保证先父类再子类。
父子类的无参构造函数(自动调用):
如果子类构造器没有显式地调用父类的构造器,则将自动调用父类的默认(没有参数)的构造器。即编译器尝试在子类构造器(可以是有参或者无参构造器)的首行自动加上super()。(如果父类没有无参构造器就需要手动调用父类的有参构造器。见下边一条)
父子类的有参构造函数(手动调用):
如果有继承关系,父类只有有参构造方法(如果既有有参构造器又有无参构造器则不符合这个条件),那么子类必须在构造器中(有参或者无参构造器)中第一行显式的调用父类的有参构造方法即super(参数)。
例子如下
1.无参构造器中调用父类有参构造器
package com.example.ABReference;
class SuperClass {
public int a=10;
public SuperClass(int a) {
}
}
public class SubClass extends SuperClass {
public SubClass() {
super(5);
}
public static void main(String[] args) {
SuperClass t = new SubClass();
}
}
但是下面写法编译器会报错
package com.example.ABReference;
class SuperClass {
public int a=10;
public SuperClass(int a) {
}
}
public class SubClass extends SuperClass {
int b=9;
public SubClass() {
super(b);
}
public static void main(String[] args) {
SuperClass t = new SubClass();
}
}
上面代码会报出:无法在调用超类构造器之前引用b,调用父类构造器之前连b的引用(引用是解析的过程)都没有。更加印证了以下顺序: 子类构造器调用super(b)=>父类实例变量初始化=>子类实例变量初始化=>子类构造函数
- 可以在子类有参构造器中调用父类构造器,super(参数)
package com.example.ABReference;
class SuperClass {
public int a=10;
public SuperClass(int a) {
}
}
public class SubClass extends SuperClass {
public SubClass(int a) {
super(a);
}
public static void main(String[] args) {
SuperClass t = new SubClass(5);
}
}
如果父类没有不带参数的构造器(即只有有参构造器),并且在子类的构造器中又没有显式地调用父类的构造器,则java编译器将报告错误
final、静态方法、private方法不能重写。private构造函数只能系统调用。系统(实现如ioc容器,通过反射实现)可以调用有参、无参、private构造方法来实例化对象。
实际应用中经常用到spring ioc将Spring Ioc的注入顺序考虑进去
实例化过程:
jvm的实例属性注入=>构造方法=》@Autowired @Value注入实例属性(可以看成同时) =>@Value注入static属性,利用set方法注入(static属性的@Value方式注入,必须用set方法,否则无效)=>@PostConstruct方法
package cm.soft.collect.config;
import cm.soft.collect.biz.CollectBiz;
import cm.soft.collect.util.OnMqttCallBack;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
@PropertySource("classpath:/application.yml")
public class MqttConfig implements AutoCloseable {
/*****jvm 类和对象初始化 ******/
/**
* 第一步加载,先加载static,类变量,堆中的方法区(非堆区,永久代)
*/
private static String username = "admin";
private static String password = "J2nUR40e2P0qjL87";
/**
* 第二步加载实例变量,堆中的堆区
*/
private int qos = 0;// 默认等级
private static int qoa;
/**
* 第三步加载构造方法
*/
public MqttConfig(@Value("${mqtt.qoa}") int qoa) {// 可以通过构造注入
this.qoa = qoa;
this.disconnectedBufferOptions = new DisconnectedBufferOptions();
this.options = new MqttConnectOptions();
this.persistence = new MemoryPersistence();
this.executorService = Executors.newSingleThreadExecutor();
}
private final MemoryPersistence persistence;
private final ExecutorService executorService;// 启动关闭连接
private final MqttConnectOptions options;// 连接选项
private final DisconnectedBufferOptions disconnectedBufferOptions;// 初始化连接必须
private volatile IMqttAsyncClient mqttClient;
/***** spring注入 放在jvm后边减少侵入 ******/
/**
* 第四步加载 @Value @Autowired (可以认为同时注入,必要时再研究先后)
*/
@Value("${mqtt.subTopics}")
private String SUB_TOPICS;
@Value("${mqtt.topic}")
private String defaultTopic;
@Value(("${mqtt.brokerUrl}"))
private String brokerUrl;
@Autowired
LogConfig log;
@Autowired
CollectBiz collectBiz;
/**
* 第五步 setValue @Value
*/
private static String clientId;
@Value("${mqtt.clientId}")
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* 第六步加载@PostConstruct
* postcontruct初始化options和disconnectedBufferOptions,
* 因为执行构造方法的时候brokerUrl,username属性还未注入,所以不能放在构造方法中
* 实例化顺序应该是 jvm的实例属性注入=》构造方法=》@Autowired @Value注入实例属性(可以看成同时)
* =》@Value注入static属性,利用set方法注入(static属性的@Value方式注入,必须用set方法或者构造方法注入,否则无效)=》@PostConstruct方法
*/
@PostConstruct
void init() {
// 初始化options
options.setServerURIs(new String[]{brokerUrl});
options.setUserName(username);
options.setPassword(password.toCharArray());
// 设置超时时间
options.setConnectionTimeout(10);
// 设置会话心跳时间
options.setKeepAliveInterval(20);
// 保持会话
options.setCleanSession(true);
options.setAutomaticReconnect(true);
//表示允许多大数量的QoS为1或2消息被同时进行传输处理。这些消息包括正在进行握手的消息和进行重新发送的消息。默认为20个,
//如果设置为0,表示不设限制;如果为1,则会确保消息被顺序处理。
// options.setMaxInflight();
// 初始化disconnectedBufferOptions
disconnectedBufferOptions.setBufferEnabled(true);
// 初始化mqttClient
if (mqttClient == null) {
mqttClient = getMqqtClient();
}
((MqttAsyncClient)mqttClient).setBufferOpts(disconnectedBufferOptions);
// 设置mqttClient的回调
mqttClient.setCallback(new OnMqttCallBack(log, this, collectBiz));
// 初始化连接
connect();
}
如果需要手动控制spring实例的加载顺序,和依赖关系。
- 可以添加配置类,并用@DependsOn注解进行控制。
面对如下需求:
CollectBiz类中的saveDataToSql方法会在@Autowired MachineCurrentProcessBizState machineCurrentProcessForVersionBizState; @Autowired MachineProcessOnChangedBizState machineProcessOnChangedBizState; @Autowired MachineEjectionBizState machineEjectionBizState;
这些属性注入之前执行,所以machineProcessOnChangedBizState machineProcessOnChangedBizState machineEjectionBizState都是Null,程序会报错,所以必须保证CollectBiz实例化之前上述属性必须注入。 - 还有一种解决方式,通过方法注入的方式。
public class CollectBiz {
@Autowired
MqttConfig mqttConfig;
@Autowired
MachineCurrentProcessBizState machineCurrentProcessForVersionBizState;
@Autowired
MachineProcessOnChangedBizState machineProcessOnChangedBizState;
@Autowired
MachineEjectionBizState machineEjectionBizState;
/**
* 先进行订阅具体的主题
*
* @throws MqttException
*/
public void subscribe() throws MqttException {
mqttConfig.subscribe("V_2/+/+/+/+/+/+/+/+/+");
}
public void saveDataToSql(String topic, MqttMessage mqttMessage) {
BizType bizType = BizType.filterCollectType(topic);
if (bizType != null) {
if (bizType.equals(BizType.SAVE_MACHINE_PROCESS_ON_CHANGED_ToSQL)) {
machineProcessOnChangedBizState.saveDataToSql(topic, mqttMessage);
} else if (bizType.equals(BizType.SAVE_MACHINE_CURRENT_PROCESS_FOR_VERSION_ToSQL)) {
machineCurrentProcessForVersionBizState.saveDataToSqlOnRequest(topic, mqttMessage);
} else if (bizType.equals(BizType.SAVE_MACHINE_EJECTION_DATA_ToSQL)) {
machineEjectionBizState.saveDataToSql(topic, mqttMessage);
}
}
}
public void realTimeCalculate(String topic, MqttMessage mqttMessage) {
BizType bizType = BizType.filterCollectType(topic);
if (bizType != null) {
if (bizType.equals(BizType.SAVE_MACHINE_PROCESS_ON_CHANGED_ToSQL)) {
// machineProcessOnChangedBizState.saveDataToSql(topic, mqttMessage);
} else if (bizType.equals(BizType.SAVE_MACHINE_CURRENT_PROCESS_FOR_VERSION_ToSQL)) {
// machineCurrentProcessForVersionBizState.saveDataToSqlOnRequest(topic, mqttMessage);
} else if (bizType.equals(BizType.SAVE_MACHINE_EJECTION_DATA_ToSQL)) {
machineEjectionBizState.realtimeCalCuLate(topic, mqttMessage);
}
}
}
}
@Configuration
public class CollectBizConfig {
@Bean
public MachineCurrentProcessBizState machineCurrentProcessBizState() {
return new MachineCurrentProcessBizState();
}
@Bean
public MachineProcessOnChangedBizState machineProcessOnChangedBizState() {
return new MachineProcessOnChangedBizState();
}
@Bean
public MachineEjectionBizState machineEjectionBizState() {
return new MachineEjectionBizState();
}
@Bean
@DependsOn(value = {"machineCurrentProcessBizState","machineProcessOnChangedBizState","machineEjectionBizState"})
public CollectBiz collectBiz() {
return new CollectBiz();
}
}
或者另一种方式方法注入的方式:CollectBiz依赖于OnMqttCallBack
@Component
public class CollectBiz {
private OnMqttCallBack mqttCallBack;
@Autowired
public void setMqttCallBack(OnMqttCallBack onMqttCallBack) {
this.mqttCallBack = onMqttCallBack;
mqttCallBack.setMessageArrives(iMessageArrives);
}
}
变量的分类和初始化
变量包括: 1.类的属性(类中又有属性、字段等叫法,属性还可以是类中的方法,这里认为是字段。类的属性又分为static属性和instance属性) 2.方法里的局部变量 3.方法的参数
- 对于第一种变量,Java虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。
int类型变量默认初始值为0 float类型变量默认初始值为0.0f double类型变量默认初始值为0.0 boolean类型变量默认初始值为false char类型变量默认初始值为0(ASCII码) long类型变量默认初始值为0
所有对象引用类型变量默认初始值为null,即不指向任何对象。注意数组本身也是对象,所以没有初始化的数组引用在自动初始化后其值也是null。
对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。
- 对于第二种变量,必须明确地进行初始化。如果再没有初始化之前就试图使用它,编译器会抗议。
如果初始化的语句在try块中或if块中,也必须要让它在第一次使用前一定能够得到赋值。也就是说,把初始化语句放在只有if块的条件判断语句中编译器也会抗议,因为执行的时候可能不符合if后面的判断条件,如此一来初始化语句就不会被执行了,这就违反了局部变量使用前必须初始化的规定。但如果在else块中也有初始化语句,就可以通过编译,因为无论如何,总有至少一条初始化语句会被执行,不会发生使用前未被初始化的事情。对于try-catch也是一样,如果只有在try块里才有初始化语句,编译不通过。如果在catch或finally里也有,则可以通过编译。总之,要保证局部变量在使用之前一定被初始化了。所以,一个好的做法是在声明他们的时候就初始化他们,如果不知道要出事化成什么值好,就用上面的默认值吧!
- 其实第三种变量和第二种本质上是一样的,都是方法中的局部变量。只不过作为参数,肯定是被初始化过的,传入的值就是初始值,所以不需要初始化。
一段函数和图片,说明类的加载,对象初始化,函数执行过程
首先AppClassLoader加载MainApp类型,找到入口main方法,MainApp类会被初始化,执行main字节码,会有一个线程来执行字节码。此时生成一个栈帧入java虚拟机栈,栈帧包括局部变量表,操作数栈、动态解析、方法出入口等信息)线程执行过程中,需要用到Animal类,这时发现Animal对象还未加载,那么接下来会执行< client >方法加载Animal类的class文件到方法区(这时系统会再堆中生成一个单例Class对象,对应方法区的Animal类型结构)。然后执行< init >无参构造器,进行对象的实例化。实例化前先在堆中分配内存,根据方法区的Animal类信息,Animal类对象内存大小是实现确定的。(堆内存的分配有不同JVM实现方式,如果使用和空闲的内存空间分开是指针碰撞,如果使用和空闲的内存空间不分开则是空闲列表。)animal对象初始化后,根据对象头的Klass指针,找到方法区对应的类结构信息,根据方法表运行printFrame()函数,接下来,又生成一个栈帧(Animal对象引用animal加载进栈帧中的局部变量表中,指向堆内存中的Animal对象)入java虚拟机栈的栈顶。Java虚拟机会机将常量池内的符号引用(函数方法名,类型名还是其他字面量)替换为直接引用如指针,从而可以进行方法,类型定位,这个过程是解析。
MainApp类在方法区以及Animal在方法区都分别有运行时常量池、类型信息、方法名、变量名等信息,图中没有画全。堆中的pointer即是对象头中存储的Kcass指针,指向了方法区。animal对象引用存储在java栈中。
package com.example.ABReference;
public class MainApp {
public static void main(String[] args) {
Animal animal=new Animal();
animal.printFrame();
}
}
class Animal {
void printFrame(){
System.out.println("打印");
}
}
扩展:类文件中到底存了什么
对一个类编译后的class文件(二进制文件)javap进行反汇编
内容大致分为4个部分:
第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。
第二部分:显示了该类中所涉及到常量池。
第三部分:显示该类的构造器,编译器自动插入的 Object和StackFrame构造器。
第四部分:显示了main方的信息。
第五部分:显示了add方的信息。
package com.example.ABReference;
public class StackFrame {
public static void main(String[] args) {
add(1, 2);
}
private static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}
}
/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/bin/javap -c -verbose -c -p com.example.ABReference.StackFrame
Classfile /Users/smilezmh/Desktop/work/test/target/classes/com/example/ABReference/StackFrame.class
Last modified 2020-8-6; size 608 bytes
MD5 checksum 9391fa779e500bb648422d193a20414c
Compiled from "StackFrame.java"
public class com.example.ABReference.StackFrame
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#25 // java/lang/Object."<init>":()V
#2 = Methodref #3.#26 // com/example/ABReference/StackFrame.add:(II)I
#3 = Class #27 // com/example/ABReference/StackFrame
#4 = Class #28 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/example/ABReference/StackFrame;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 MethodParameters
#17 = Utf8 add
#18 = Utf8 (II)I
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 StackFrame.java
#25 = NameAndType #5:#6 // "<init>":()V
#26 = NameAndType #17:#18 // add:(II)I
#27 = Utf8 com/example/ABReference/StackFrame
#28 = Utf8 java/lang/Object
{
public com.example.ABReference.StackFrame();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/ABReference/StackFrame;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V //返回值类型
flags: ACC_PUBLIC, ACC_STATIC //访问修饰符
Code:
stack=2, locals=1, args_size=1 //依次为,栈的大小,局部变量表的大小,参数的个数
0: iconst_1
1: iconst_2
2: invokestatic #2 //去常量池中引用"#2"符号引用的类与方法 // Method add:(II)I
5: pop
6: return
LineNumberTable: // LineNumberTable 为代码行号与字节码行号的对应关
line 5: 0
line 6: 6
LocalVariableTable: // 局部变量表信息
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
MethodParameters:
Name Flags
args
private static int add(int, int);
descriptor: (II)I //返回值类型
flags: ACC_PRIVATE, ACC_STATIC //访问修饰符
Code:
stack=2, locals=3, args_size=2 //依次为,栈位置使用个数,局部变量表位置使用个数,参数的个数
0: iconst_0 // 将int类型的0压入栈顶
1: istore_2 // 将栈顶的数据0存储到本地变量表第2个本地变量的位置(从第0个开始存数据,头上是this指针),栈顶元素出站消失。
2: iload_0 // 将第0个本地变量推至栈顶,第0个本地本地变量的数据不会消失
3: iload_1 // 将第1个本地变量推至栈顶,第1个本地本地变量的数据不消失,上一步的栈顶的数据下移。
4: iadd // 将栈中两int数据相加,加之后的数据3,放到栈顶
5: istore_2 // 将栈顶数据3放到本地变量表第2个位置,栈顶元素消失。
6: iload_2 // 将第2个本地变量表的元素推到栈顶,第2个本地本地变量的数据不会消失
7: ireturn // 返回3
LineNumberTable: // java代码行号和字节码行号的对应关系
line 9: 0 // java 第9行 int c = 0;对应第0行 0: iconst_0
line 10: 2 // java 第10行 c = a + b;;对应第2行 2: iload_0
line 11: 6
LocalVariableTable: // 局部变量表 开始 长度 占几个Slot 变量名 参数类型签名(数组[ I基本类型int 泛型等等类型信息)
Start Length Slot Name Signature
0 8 0 a I
0 8 1 b I
2 6 2 c I
MethodParameters: // 方法参数 名字 标记
Name Flags
a
b
}
SourceFile: "StackFrame.java"
add(1,2)的过程
常量池
Constant 说明
CONSTANT_Class 类或接口的符号引用
CONSTANT_Methodref 类中方法的符号引用
CONSTANT_InterfaceMethodref 接口中方法的符号引用
CONSTANT_String 字符串类型常量
CONSTANT_Integer 整形常量
CONSTANT_Float 浮点型常量
CONSTANT_Long 长整型常量
CONSTANT_Double 双精度浮点型常量
CONSTANT_NameAndType 字段或方法的符号引用
CONSTANT_Utf8 UTF-8编码的字符串
CONSTANT_MethodHandle 表示方法句柄
CONSTANT_MethodType 标志方法类型
CONSTANT_InvokeDynamic 表示一个动态方法调用点
CONSTANT_Fieldref 字段的符号引用
this指针
1.子类持有父类指针,即this可以转换成super,当前类找不到成员变量(字段或方法)就去父类找。
2.字段不体现多态特性,调用方法才体现多态。
3.多态定义:针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法
例证:
package com.example.test2;
public class Person {
protected String name;
public Person(String name) {
System.out.println(this);
this.name = name;
// 不论怎样this.name就是指向当前类(protected String name)
//如果当前类没有这个字段name,this转换成super,就会去父类找这个成员变量
System.out.println(this.name);
run();// 体现多态:this指向子类。
}
public void run() {
System.out.println("per run");
}
}
package com.example.test2;
public class Student extends Person {
public String name;// 0* 如果这一行注释掉,在1*位置 this.name在当前类中找不到name字段,就会去父类找。this可以转换成super。子类持有父类指针。
public Student(String name, String name1) {
super(name);
this.name = name1;// 1* 注意如果注释掉0*,那么this.name在当前类中找不到name属性,this会转成super去找父类Person中的name属性,这时应该注意Person中name的访问属性,如果是private那么会报错,应该确保父类中至少是protected访问属性(或者默认default,同一个包中也可以访问,包即一个文件夹中)
}
public void run() {
System.out.println("stu run");
}
}
package com.example.test2;
public class Main {
public static void main(String[] args) {
Person p = new Student("per", "stu");
System.out.println(p.name);
System.out.println(((Student) p).name);
System.out.println("^^^^^^^");
Person p1 = new Person("person");
}
}
结果
com.example.test2.Student@a09ee92
per
stu run
per
stu
^^^^^^^
com.example.test2.Person@30f39991
person
per run
函数(栈)参数传递
值传递:传值,实际是把实参的值赋值给形参,相当于copy。
那么对形参的修改,不会影响实参的值。
引用传递:实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值,那么传地址以后,实参和行参都指向同一个对象,因此对形参的修改会影响到实参。
传值调用中,只使用了实参的值。传值调用机制里,形参是一个局部变量,其初始值为相应实参的值。在引用调用机制里,将实参(JVM堆内存的堆区)的地址传递给形参(笼统说JVM内存模型的本地区的栈内存的栈帧中),因此任何发生在形参上的改变实际上都发生在实参变量上(堆内存)。形参实际上是局部变量,声明周期在方法内部。
引用传递是将原实际引用变量的内存地址(堆内存地址)传递给方法的局部变量,执行方法,操作局部变量时(会将局部变量表局部变量加载到操作数栈),并找到局部变量对应的实际变量(堆内存),对其进行操作。会对原引用变量(堆内存)造成影响。可以将一个变量通过引用传递给函数,这样该函数就可以修改其参数的值。