类初始化

在讲类的初始化之前,我们先来大概了解一下类的声明周期。如下图

类的声明周期可以分为7个阶段,但今天我们只讲初始化阶段。我们我觉得出来使用卸载阶段外,初始化阶段是最贴近我们平时学的,也是笔试做题过程中最容易遇到的,假如你想了解每一个阶段的话,可以看看深入理解Java虚拟机这本书。

下面开始讲解初始化过程。

注意:

这里需要指出的是,在执行类的初始化之前,其实在准备阶段就已经为类变量分配过内存,并且也已经设置过类变量的初始值了。例如像整数的初始值是0,对象的初始值是null之类的。基本数据类型的初始值如下:

数据类型 初始值 数据类型 初始值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char ‘\u0000’ reference null
byte (byte)0

大家先想一个问题,当我们在运行一个java程序时,每个类都会被初始化吗?假如并非每个类都会执行初始化过程,那什么时候一个类会执行初始化过程呢?

答案是并非每个类都会执行初始化过程,你想啊,如果这个类根本就不用用到,那初始化它干嘛,占用空间。

至于何时执行初始化过程,虚拟机规范则是严格规定了有且只有5中情况会马上对类进行初始化

  1. 当使用new这个关键字实例化对象、读取或者设置一个类的静态字段,以及调用一个类的静态方法时会触发类的初始化(注意,被final修饰的静态字段除外)。
  2. 使用java.lang.reflect包的方法对类进行反射调用时,如果这个类还没有进行过初始化,则会触发该类的初始化。
  3. 当初始化一个类时,如果其父类还没有进行过初始化,则会先触发其父类。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,如果一个…..(省略,说了也看不懂,哈哈)。

注意是有且只有。这5种行为我们称为对一个类的主动引用

初始化过程

类的初始化过程都干了些什么呢?

在类的初始化过程中,说白了就是执行了一个类构造器()方法过程。注意,这个clinit并非类的构造函数(init())。

至于clinit()方法都包含了哪些内容?

实际上,clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序则是由语句在源文件中出现的顺序来决定的。并且静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。如下面的程序。

public class Test1 {
static {
t = 10;//编译可以正常通过
System.out.println(t);//提示illegal forward reference错误
}
static int t = 0;
}

给大家抛个练习

public class Father {
public static int t1 = 10;
static {
t1 = 20;
}
}
class Son extends Father{
public static int t2 = t1;
}
//测试调用
class Test2{
public static void main(String[] args){
System.out.println(Son.t2);
}
}

输出结果是什么呢?

答案是20。我相信大家都知道为啥。因为会先初始化父类啊。

不过这里需要注意的是,对于类来说,执行该类的clinit()方法时,会先执行父类的clinit()方法,但对于接口来说,执行接口的clinit()方法并不会执行父接口的clinit()方法。只有当用到父类接口中定义的变量时,才会执行父接口的clinit()方法。

被动引用

上面说了类初始化的五种情况,我们称之为称之为主动引用。居然存在主动,也意味着存在所谓的被动引用。这里需要提出的是,被动引用并不会触发类的初始化。下面,我们举例几个被动引用的例子:

  1. 通过子类引用父类的静态字段,不会触发子类的初始化
/**
* 1.通过子类引用父类的静态字段,不会触发子类的初始化
*/
public class FatherClass {
//静态块
static {
System.out.println("FatherClass init");
}
public static int value = 10;
} class SonClass extends FatherClass {
static {
System.out.println("SonClass init");
}
}
class Test3{
public static void main(String[] args){
System.out.println(SonClass.value);
}
}

输出结果

FatherClass init

说明并没有触发子类的初始化

  1. 通过数组定义来引用类,不会触发此类的初始化。
 class Test3{
public static void main(String[] args){
SonClass[] sonClass = new SonClass[10];//引用上面的SonClass类。
}
}

输出结果是啥也没输出。

  1. 引用其他类的常量并不会触发那个类的初始化
public class FatherClass {
//静态块
static {
System.out.println("FatherClass init");
}
public static final String value = "hello";//常量
} class Test3{
public static void main(String[] args){
System.out.println(FatherClass.value);
}
}

输出结果:hello

实际上,之所以没有输出”FatherClass init”,是因为在编译阶段就已经对这个常量进行了一些优化处理,例如,由于Test3这个类用到了这个常量”hello”,在编译阶段就已经将”hello”这个常量储存到了Test3类的常量池中了,以后对FatherClass.value的引用实际上都被转化为Test3类对自身常量池的引用了。也就是说,在编译成class文件之后,两个class已经没啥毛关系了。


重载

对于重载,我想学过java的都懂,但是今天我们中虚拟机的角度来看看重载是怎么回事。

首先我们先来看一段代码:

//定义几个类
public abstract class Animal {
}
class Dog extends Animal{
}
class Lion extends Animal{
} class Test4{
public void run(Animal animal){
System.out.println("动物跑啊跑");
}
public void run(Dog dog){
System.out.println("小狗跑啊跑");
}
public void run(Lion lion){
System.out.println("狮子跑啊跑");
}
//测试
public static void main(String[] args){
Animal dog = new Dog();
Animal lion = new Lion();;
Test4 test4 = new Test4();
test4.run(dog);
test4.run(lion);
}
}

运行结果:

动物跑啊跑

动物跑啊跑

相信大家学过重载的都能猜到是这个结果。但是,为什么会选择这个方法进行重载呢?虚拟机是如何选择的呢?

在此之前我们先来了解两个概念。

先来看一行代码:

Animal dog = new Dog();

对于这一行代码,我们把Animal称之为变量dog的静态类型,而后面的Dog称为变量dog的实际类型

所谓静态类型也就是说,在代码的编译期就可以判断出来了,也就是说在编译期就可以判断dog的静态类型是啥了。但在编译期无法知道变量dog的实际类型是什么。

现在我们再来看看虚拟机是根据什么来重载选择哪个方法的。

对于静态类型相同,但实际类型不同的变量,虚拟机在重载的时候是根据参数的静态类型而不是实际类型作为判断选择的。并且静态类型在编译器就是已知的了,这也代表在编译阶段,就已经决定好了选择哪一个重载方法。

由于dog和lion的静态类型都是Animal,所以选择了run(Animal animal)这个方法。

不过需要注意的是,有时候是可以有多个重载版本的,也就是说,重载版本并非是唯一的。我们不妨来看下面的代码。

public class Test {
public static void sayHello(Object arg){
System.out.println("hello Object");
}
public static void sayHello(int arg){
System.out.println("hello int");
}
public static void sayHello(long arg){
System.out.println("hello long");
}
public static void sayHello(Character arg){
System.out.println("hello Character");
}
public static void sayHello(char arg){
System.out.println("hello char");
}
public static void sayHello(char... arg){
System.out.println("hello char...");
}
public static void sayHello(Serializable arg){
System.out.println("hello Serializable");
} //测试
public static void main(String[] args){
char a = 'a';
sayHello('a');
}
}

运行下代码。
相信大家都知道输出结果是

hello char

因为a的静态类型是char,随意会匹配到sayHello(char arg);

但是,如果我们把sayHello(char arg)这个方法注释掉,再运行下。

结果输出:

hello int

实际上这个时候由于方法中并没有静态类型为char的方法,它就会自动进行类型转换。‘a’除了可以是字符,还可以代表数字97。因此会选择int类型的进行重载。

我们继续注释掉sayHello(int arg)这个方法。结果会输出:

hello long。

这个时候’a’进行两次类型转换,即 ‘a’ -> 97 -> 97L。所以匹配到了sayHell(long arg)方法。

实际上,’a’会按照char ->int -> long -> float ->double的顺序来转换。但并不会转换成byte或者short,因为从char到byte或者short的转换是不安全的。(为什么不安全?留给你思考下)

继续注释掉long类型的方法。输出结果是:

hello Character

这时发生了一次自动装箱,’a’被封装为Character类型。

继续注释掉Character类型的方法。输出

hello Serializable

为什么?

一个字符或者数字与序列化有什么关系?实际上,这是因为Serializable是Character类实现的一个接口,当自动装箱之后发现找不到装箱类,但是找到了装箱类实现了的接口类型,所以在一次发生了自动转型。

我们继续注释掉Serialiable,这个时候的输出结果是:

hello Object

这时是’a’装箱后转型为父类了,如果有多个父类,那将从继承关系中从下往上开始搜索,即越接近上层的优先级越低。

继续注释掉Object方法,这时候输出:

hello char…

这个时候’a’被转换为了一个数组元素。

从上面的例子中,我们可以看出,元素的静态类型并非就是一定是固定的,它在编译期根根据优先级原则来进行转换。其实这也是java语言实现重载的本质

重写

我们先来看一段代码

//定义几个类
public abstract class Animal {
public abstract void run();
}
class Dog extends Animal{
@Override
public void run() {
System.out.println("小狗跑啊跑");
}
}
class Lion extends Animal{
@Override
public void run() {
System.out.println("狮子跑啊跑");
}
}
class Test4{
//测试
public static void main(String[] args){
Animal dog = new Dog();
Animal lion = new Lion();;
dog.run();
lion.run();
}
}

运行结果:

小狗跑啊跑
狮子跑啊跑

我相信大家对这个结果是毫无疑问的。他们的静态类型是一样的,虚拟机是怎么知道要执行哪个方法呢?

显然,虚拟机是根据实际类型来执行方法的。我们来看看main()方法中的一部分字节码

//声明:我只是挑出了一部分关键的字节码
public static void (java.lang.String[]);
Code:
Stack=2, Locals=3, Args_size=1;//可以不用管这个
//下面的是关键
0:new #16;//即new Dog
3: dup
4: invokespecial #18; //调用初始化方法
7: astore_1
8: new #19 ;即new Lion
11: dup
12: invokespecial #21;//调用初始化方法
15: astore_2 16: aload_1; 压入栈顶
17: invokevirtual #22;//调用run()方法
20: aload_2 ;压入栈顶
21: invokevirtual #22;//调用run()方法
24: return

解释一下这段字节码:

0-15行的作用是创建Dog和Lion对象的内存空间,调用Dog,Lion类型的实例构造器。对应的代码:

Animal dog = new Dog();

Animal lion = new Lion();

接下来的16-21句是关键部分,16、20两句分分别把刚刚创建的两个对象的引用压到栈顶。17和21是run()方法的调用指令。

从指令可以看出,这两条方法的调用指令是完全一样的。可是最终执行的目标方法却并不相同。这是为啥?

实际上:

invokevirtual方法调用指令在执行的时候是这样的:

  1. 找到栈顶的第一个元素所指向的对象的实际类型,记作C.
  2. 如果类型C中找到run()这个方法,则进行访问权限的检验,如果可以访问,则方法这个方法的直接引用,查找结束;如果这个方法不可以访问,则抛出java.lang.IllegalAccessEror异常。
  3. 如果在该对象中没有找到run()方法,则按照继承关系从下往上对C的各个父类进行第二步的搜索和检验。
  4. 如果都没有找到,则抛出java.lang.AbstractMethodError异常。

所以虽然指令的调用是相同的,但17行调用run方法时,此时栈顶存放的对象引用是Dog,21行则是Lion。

这,就是java语言中方法重写的本质。

本次的讲解到此结束,希望对你有所帮助。

从jvm角度看懂类初始化、方法重写、重载。的更多相关文章

  1. 【转】两道面试题,带你解析Java类加载机制(类初始化方法 和 对象初始化方法)

    本文转自 https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html 关键语句 我们只知道有一个构造方法,但实际上Ja ...

  2. 从JVM角度看Java多态

    首先,明确一下,Java多态的三个必要条件: 1. 继承 2. 子类重写父类方法 3. 父类引用指向子类对象 然后看一个例子 package test.xing; class Father{ prot ...

  3. 看懂类图——UML类图基础

    类图 要学懂设计模式,就需要先看得懂类图,类与类之间的关系是学习设计模式的基础,而在软件工程中,类与类之间的关系是通过UML中的类图来体现. 这篇笔记包含的不会是类图的所有东西,包含的只是各个类之间的 ...

  4. (转)看懂类图——UML类图基础

    类图 要学懂设计模式,就需要先看得懂类图,类与类之间的关系是学习设计模式的基础,而在软件工程中,类与类之间的关系是通过UML中的类图来体现. 这篇笔记包含的不会是类图的所有东西,包含的只是各个类之间的 ...

  5. C语言-人狼羊菜问题-最容易看懂的解决方法及代码

    题目描述:农夫需要把狼.羊.菜和自己运到河对岸去,只有农夫能够划船,而且船比较小,除农夫之外每次只能运一种东西,还有一个棘手问题,就是如果没有农夫看着,羊会偷吃菜,狼会吃羊.请考虑一种方法,让农夫能够 ...

  6. 一文看懂神经网络初始化!吴恩达Deeplearning.ai最新干货

    [导读]神经网络的初始化是训练流程的重要基础环节,会对模型的性能.收敛性.收敛速度等产生重要的影响.本文是deeplearning.ai的一篇技术博客,文章指出,对初始化值的大小选取不当,  可能造成 ...

  7. 接口、抽象类、方法复写、类Equals方法重写

    接口: /* * Java接口中的數據成員必須初始化,該成員有隱藏的final.satic.常量, * 一次賦值后不可在賦值 * 成員方法訪問修飾符必須是公共修飾符,可以顯示聲明也可以不聲明 * 成員 ...

  8. python干货-类属性和方法,类的方法重写

    类属性与方法 类的私有属性 __private_attrs: 两个下划线开头,表明为私有,外部不可用,内部使用时self.__private_attrs. 类的方法 在类的内部,使用 def 关键字来 ...

  9. [转]从JVM角度看线程安全与垃圾收集

    线程安全 Java内存模型中,程序(进程)拥有一块内存空间,可以被所有的线程共享,即MainMemory(主内存):而每个线程又有一块独立的内存空间,即WorkingMemory(工作内存).普通情况 ...

随机推荐

  1. 推荐vim学习教程--《Vim 练级手册》

    非常不错的vim学习资源,讲解的简单明了,可以作为速查工具,在忘记时就翻下.地址如下: <Vim 练级手册>

  2. Python函数参数&time、OS、json模块

    ##可变参数 PORT = 3306 #常量 def mysql(host,user,password,port,charset,sql,db): print('连接mysql') # mysql(' ...

  3. PHP 清除 Excel 导入的数据空格

    处理excel中的数据时,遇到了页面中显示为空格,审查元素时却显示为换行,使用replace函数也不管用,反正就是不知道是什么东西,看起来像空格 中文空格这里面有好几种:没有简单的解决问题的方式,比如 ...

  4. Tomcat手动部署Web项目详细步骤

    阅读须知:文章基于Tomcat8,其它版本若有差异,请自行辨别.本文为博主原创文章,转载请附原文链接. 不借助任何IDE,这里介绍在Tomcat中手动部署web项目的三种方式: 1.部署解包的weba ...

  5. dedecms自定义模型内容调用多个Ueditor

    关于dedecms后台如何整合百度编辑器(ueditor)网上有很多了,本站就不再赘述了,主要问题是,涉及到如果有内容模型的修改,则按照网络上介绍的方法会发现有BUG.当修改过默认的文章模型或者其他模 ...

  6. Ubuntu下安装Pycharm出现unsupported major.minor version 52.0

    (一)原因 Ubuntu下pycharm安装:https://jingyan.baidu.com/article/60ccbceb4e3b0e64cab19733.html pycharm激活:htt ...

  7. Ubuntu出现卡logo、卡住、黑屏无法正常启动、屏幕和键盘背光无法调节等一系列问题?可能是NVIDIA显卡驱动没装好

    也不知道是幸运还是不幸,我从一开始接触ubuntu就遇到这一系列的问题, 而且一直没有一个彻底解决的办法,搞得我无比头疼,也害得我重装了无数遍系统... 国际惯例,只按照个人习惯和喜好来写,对某些人来 ...

  8. C#相对路径

    1. 根目录 .\\ 或者直接给出文件名称,是找根目录的路径. 如:path = "gs.mdb" 与 path = ".\\gs.mdb"是一个意思. 2. ...

  9. Validator验证框架

    Validator验证框架 系统分析 在设计Validator验证框架时,需要明确以下问题. (1)当用户没有输入数据时,弹出英文提示信息. (2)当用户输入的数据长度大于系统设置的数据长度,弹出英文 ...

  10. 机器学习之正则化(Regularization)

    1. The Problem of Overfitting 1 还是来看预测房价的这个例子,我们先对该数据做线性回归,也就是左边第一张图. 如果这么做,我们可以获得拟合数据的这样一条直线,但是,实际上 ...