写在前面:这篇文章考究了大量的博文与国内外百科,以线性叙事方式解析内容,请按顺序耐心的读懂,如有疑论请不吝评论。
目录
public class AppObject {
static{
System.out.println("TT被类加载器加载");
}
}
AppObject obj = new AppObject();
这是创建一个Java对象最基础的过程,
如果我们可以多停留一秒的思考,就会发现这一句编码,竟有4个组成部分:
通常我们只关注获取对象之后可以进行的事情,实际上在这一句Java代码中,做了十分多的事情。
不妨将这段编码的有效组成拓扑展开,可以得到3个过程:
-
类的编译
public class AppObject {
static{
System.out.println("TT被类加载器加载");
}
}
- 对象的声明
AppObject obj;
- 对象的实例化
obj = new AppObject();
具体的流程如下:
类的编译
public class AppObject {
public AppObject() {
}
static {
System.out.println("TT被类加载器加载");
}
}
运行启动后,IDE首先会根据javac命令编译项目下所有的Java文件为.class文件,这是一种16进制的字节码文件,如果用文本编辑器打开查看,会发现开头的魔数是"cafe babe",这是可被JVM读取的通行证,如果用IDE查看,会发现生成了一个缺省的构造方法。
这里值得一提的是,对于各种引用包括:对象、变量、方法等,目前阶段无法得知引用的具体存储地址,因此用符号引用暂时代替。
对象的声明
public static void main(String[] args) {
AppObject obj;
}
运行主程序,这时你是否认为,JVM在内存上已经开辟了一个空间,预留给后续实例与引用对象关联?
继续看.class文件:
会发现声明并没有被编译,实际上不难理解,所有语言最终都会被解释为机器语言,在机器语言中,声明引用会为其分配一块内存空间,用以后续指向其他地址,如果没有后续初始化,将会造成资源浪费,Java在编译这一步就做了一定的性能优化。
对象的实例化
对象的实例化包括两个部分:类的实例化,引用指向实例
obj = new AppObject();
类的实例化
new AppObject()
类的实例化是一个统称,实际上由很多过程组成,由JVM主要完成。
首先, 在委托JVM前,我们已有的数据就只有.class文件,即接口入参。
数据流:.class文件字节码->字节流
随后类加载器ClassLoader会根据环境变量加载.class文件,利用Java I/O技术,将文件中的字节码解析为字节流缓存在机器内存中,这一过程称之为:加载。
数据流:字节流接受校验
在引入字节流进入JVM之前,字节码校验器会检查那些无法执行的明显有破坏性的操作。除了系统类之外,其他类都要被校验,这一过程称之为:验证。
数据流:准入JVM字节流->机器语言
值得一提的是JVM并没有明确固定虚拟机类型,但在Oracle或JDK中委托的都是Hotspot虚拟机,HotSpot虚拟机最明显的特点就是随用随编译的特性。这是因为,Hotspot拥有解释器与JIT编译器(Just in time Complier)。
当准入JVM字节流接入解释器后,解释器会根据字节映射Map去解释字节内容,详解请参考博文:深入理解JVM之Java字节码(.class)文件详解 - 简书
内容包括很多,比较值得我们注意的是,对应上图:
- 解释出类元信息,即将类的结构体缓存至MetaSpace,相同的类名有且只解释一次,静态变量赋默认值,值得一提的是静态常量赋值,这是因为静态常量是类元信息,且直接解释为存储地址并存储数值,不同于静态变量是指针变量,指针变量是存储地址,指向地址存储数值,解析前是符号引用,无法完成变量赋值
- 解释出常量信息到常量池,这里说的常量是指形如封装类的默认值、自定义的常量数据等
- 解释出方法元,即类结构体只是字段以某种结构组织起来,方法无法简单以字段组织,需要同类的结构体一样,根据方法名将结构体缓存至MetaSpace
以上的过程称之为:准备。
随后,在内存上已经缓存有静态结构、具有值的静态数据后,将符号引用(见类的编译)变为直接引用,即引用变量赋值真实的物理地址。这个过程称之为:解析。
数据流:机器语言(未解释)->机器语言(数据、指令、地址)
这时MetaSpace完成了直接引用,具有了物理意义上的数据结构,开始执行静态代码(指令:赋值、寻址等),这称之为:初始化。
JIT编译器在运行时与JVM交互,当JIT编译器利用热点代码探测技术找到运行高频的代码,并将适当的字节码序列编译为本地机器代码。使用JIT编译器时,硬件可以执行本机代码,而不是让JVM重复解释相同的字节码序列,并导致翻译过程相对冗长。这样可以提高执行速度,除非方法执行频率较低。
引用指向实例
public static void main(String[] args) {
AppObject obj;
obj = new AppObject();
}
引用就是寻址,寻址就用指针变量,所以obj实际上就是main()方法下的一个局部指针变量,并在JVM栈中开辟空间,当类初始化完成后,该地址的数值存入类元信息结构体的首地址,即完成了对象的实例化。
几个类与对象的拓展的特性
反射Reflect
反射,笛卡尔曾提出:反射就是有机体对于规律性行为的应激反应。
即Java的反射机制需要有:有机体(系统)、规律性行为(触发条件)、应激反应(元信息调取)
这样理解还是模糊,不急,感觉一下你的身体就是一个系统,大脑就是存储空间,并将你所有的应激反应都载入,当触发条件满足时,即作出应激反应,你身体的某些“数据”便发生了一些变化,而这一切并不是主观发起的,是被动适应环境的一种机制。
因此,假设JVM(大脑)加载了项目下所有的类(元信息),我们即引用元信息所在的地址:
public static void main(String[] args) throws ClassNotFoundException{
Class clazz = Class.forName("com.company.AppObject");
}
类元信息是该类所有对象的共享模版,因此可以根据类元信息生产出新的对象:
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class clazz = Class.forName("com.company.AppObject");
AppObject appObject = (AppObject) clazz.newInstance();
}
那么,你肯定有个疑惑,这和new AppObject()一个有什么区别?确实,同样经历了类的加载、验证、准备、解析、初始化过程。然而new关键字做的事情就是反射机制,只不过委托JVM帮忙隐式完成这个过程。
所以,反射从JVM机制出发,允许开发者对元信息进行访问利用,降低代码的耦合,耦合?你肯定在想,为什么会有耦合问题,举个例子:我们写数据库连接的时候,假设有两种驱动MysqlDriver和OracleDriver,为了减少资源的浪费,肯定是用哪个就加载哪个,触发条件不同:
public void connect(){
//伪代码
if(MysqlDB){
//MysqlDriver实例化
}else{
//OracleDriver实例化
}
}
这样也使用了一定的反射机制,但造成了MysqlDriver与OracleDriver的耦合,这方法只能对付Mysql、Oracle两个数据库,假如需求又出现了新的DB,那将改动代码。假如使用反射机制:
public void connect(){
//伪代码
//InputStream读取配置文件,得到proporty
//XDriver实例化
Class.ForName(proporty.dbName);
//这里加载、连接、初始化了[dbName]类,同时类元信息包括了Field、Constructor、Method等信息
}
这样我们将 MysqlDriver与OracleDriver的耦合清除,利用配置文件提高了代码的模块化。
克隆(拷贝)Clone
克隆,顾名思义创造一个(equals == true)的对象,对象即引用,引用即指针变量,克隆行为即栈内存上开辟新的地址、堆内存上克隆其数据,指针变量内容即克隆对象的类元首地址。
//类获得clone行为,需要实现Cloneable协议接口
public class AppObject implements Cloneable{
int val;
TT tt;
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
public TT getTt() {
return tt;
}
public void setTt(TT tt) {
this.tt = tt;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
AppObject appObject = new AppObject();
appObject.setVal(1);
AppObject appObject2 = (AppObject) appObject.clone();
appObject.setVal(0);
AppObject appObject1 = appObject;
System.out.println("原对象与克隆对象寻址比较:"+(appObject == appObject2));
System.out.println("原对象与引用对象寻址比较:"+(appObject == appObject1));
System.out.println("原对象val:"+appObject.getVal());
System.out.println("引用对象val:"+appObject1.getVal());
System.out.println("克隆对象val:"+appObject2.getVal());
System.out.println("原对象内部引用与克隆对象内部引用寻址比较:"+(appObject.getTt() == appObject2.getTt()));
}
根据我们的推论,
- 两个对象,实例地址不同,即两个指针变量存储的地址不同,“==” 号代表存储的地址比较,故返回false。
- 两个对象的堆空间为相同数据分裂,不是同一处,因此修改appObject的字段,appObject2输出不变化。
- 由于appObject内部tt也是指针变量,存储了指向的地址,所以复制后的对象tt存储的地址不变,因此是同一处。
这称之为:浅克隆(浅拷贝)
即只对被克隆对象的栈空间、堆空间进行内容复制。
如果我们对克隆对象的内部引用也克隆:
@Override
protected Object clone() throws CloneNotSupportedException {
AppObject appObject = (AppObject) super.clone();
appObject.setTt((TT) appObject.getTt().clone());
return appObject;
}
得到的结果:
这称之为:深克隆(深拷贝)
单例类
单例类来自于设计模式中的单例模式。
单例模式 Singleton Pattern
作者认为是Java 中最简单的设计模式,
该类负责创建自己的对象,同时确保只有单个对象被创建,只公开获取实例的方法。一提到类自动创建自己的对象,我们就必须要利用到类加载过程,因此,实例需要静态,这样在类加载时便会创建一个对象,然后实例与构造方法私有,达到单例的目的。
理论可行,实践:
public class SingleObject {
static SingleObject singleObject = new SingleObject();
SingleObject() {};
public static SingleObject getInstance(){
return singleObject;
}
}
隐式创建对象
其实就是自动装箱机制。
int i = 1;
String s = 'hello';
封装函数都由JVM提供关键字并自动加载实例,一般而言,封装类的载入连接初始化,都是JVM启动时自动进行的,验证的方式也很简单,我们可以监控类的加载:
jvm参数: -XX:+TraceClassLoading
运行main():
通过这样的机制,Java还提供了一种可变参数语法 "method(类 ...){}"
public double avg( int... nums ) {
double sum = 0;
int length = nums.length;
for (int i = 0; i<length; ++i) {
sum += nums[i];
}
return sum/length;
}
avg( 2, 2, 4 );
avg( 2, 2, 4, 4 );
avg( 2, 2, 4, 4, 5, 6 );
从表面上看,函数的调用处可以传入各种离散参数参与计算,而背地里可能会隐式地产生一个对应的数组对象进行计算。
结束语
本博文将会随着作者的不断学习而更新, 如有疑论请不吝评论。