0
点赞
收藏
分享

微信扫一扫

java类中各种代码的加载时机

白衣蓝剑冰魄 2022-04-13 阅读 33
java

本篇文章主要综合了其他文章;

实例变量初始化过程

一旦一个类被加载连接初始化,他就可以随时被使用了,程序可以访问他的静态字段,调用静态方法,或者创建它的实例。在Java程序中类可以被明确或者隐含地实例化,有四种途径:明确使用new操作符;调用Class或者Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;或者通过objectInputStream类的getObject()方法反序列化。
当虚拟机创建一个新的实例时,都需要在堆中为保存对象的实例分配内存。所有在对象的类中和它的超类中声明的变量(包括隐藏的实例变量)都要分配内存。一旦虚拟机为新的对象准备好堆内存,它立即把实例变量初始化为默认的初始值。这一点很类似于类变量在连接的准备阶段赋予默认初始值,是一样的。
一旦虚拟机完成了为新的对象分配内存和为实例变量初始化为默认值后,接下来就会为实例变量赋予正确的初始值。即调用对象的实例初始化方法,在java的class文件中称之为< init >()方法,类似于类初始化的< clinit >()方法。
简单理解就是说< init >()就是从class文件字节码角度的构造函数,一般由Java代码里面的几部分构成。
超类的< init >()方法调用———————>对应super()
任意实例变量初始化方法的字节码————>对应定义变量时的赋值代码
实现了对应构造方法的方法体的字节码——>构造函数里面的代码

Java实例变量在初始化时的顺序是:
父类的初始化代码(xxx—>xxx—>xxx)—>定义变量时直接赋值—>构造函数代码块。
在这里插入图片描述

上边这个demo,结果应该输出null。分析如下:
在初始化Sub对象前,首先在堆区开辟内存并将子类中的baseName和父类中的baseName(已被隐藏)均赋为null
接下来执行对象的初始化过程,由于Sub类的构造函数没有写,初始化代码包含三部分:
super();调用Base类的init<>()
调用super()也就是Object类的init<>()
baseName = “base”;这里是父类的baseName赋值。
父类构造函数里面的:调用callName()由于该函数是在Sub类的里面调用的,所以当前的this其实是子类,由于多态调用子类Sub的callName方法此时子类的baseName变量还未赋值还是null!
baseName = “sub”;这里是子类的baseName赋值。
空(构造函数什么都没有)
所以输出null!

类的生命周期

spring中bean有生命周期,类也有生命周期;

加载

注意类加载和这个加载不是一回事,类加载包括加载,连接,初始化;

连接

包括验证,准备,解析;

验证

将xx.class二进制字节码从磁盘加载进jvm内存后,验证字节码文件是否符合规范;

准备

对类变量赋默认值;

解析

符号引用转为直接引用;
比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

初始化

如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:
1)通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
2)通过反射方式执行以上三种行为。
3)初始化子类的时候,会触发父类的初始化。
4)作为程序入口直接运行时(也就是直接调用main方法),即调用main方法时,其所在类会被触发初始化;
除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。
初始化与static相关,如类变量,static代码块等;初始化之后会对类进行实例化,此时才会给非static相关变量赋值,即执行非static相关操作;
类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。
在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。
private static int a = 3;这个类变量a在准备阶段后的值是0,将3赋值给变量a是发生在初始化阶段。

使用

分为主动使用和被动使用,后者不会进行类的初始化;被动使用包括三种情况:
1)在子类中调用父类的静态字段,只会初始化父类,不会初始化子类;
2)调用类的常量字段,不会初始化类,如被final修饰的字段;
3)定义类数组不会引起初始化;

卸载

https://www.cnblogs.com/wangsen/p/10838733.html
https://www.cnblogs.com/niuyourou/p/11892617.html

总结

做java的朋友对于对象的生命周期可能都比较熟悉,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。
写的非常好的文章:
https://blog.csdn.net/zhengzhb/article/details/7517213

类加载器

双亲委派的原理

在这里插入图片描述

应用程序类加载器只会加载上述红色框中的路径中的类;
应用程序类加载器先看是否加载了,加载了就直接返回该类,否则委托给扩展类加载器,扩展类加载器判断是否加载了,若扩展类加载器也没有加载过,则委托给引导类加载器,若引导类加载器没有加载,则再次传递给扩展类加载器,扩展类加载器再次传递给应用程序类加载器加载;
扩展类加载器和引导类加载器只能加载固定路径下的类,即jdk包中的类;所以自定义的类,扩展类和引导类加载器是无法加载的;

为啥要双亲委派?

比如自定义一个java.lang.String.java类,生成二进制字节码文件java.lang.String.class保存至磁盘,现在需要将其由磁盘加载至jvm内存,应用加载器发现自己没有加载过,则委托给扩展类加载器,一直委托给引导类加载器,于是引导类加载器在jdk核心库中发现该类已经加载进jvm内存了,于是直接返回jdk的String,于是自定义的String就不可用,即使包名相同;这样可以防止被人修改jdk类;但这样也会导致,用户不能定义和jdk类库中一样包名,名字的类,因为不会被加载到;

如何打破双亲委派机制?

如自定义的类X.class在磁盘某个路径下a/b/…下,此时可以通过自定义一个类加载器,继承ClassLoader类,重写loadClass方法实现打破双亲委派,方法中判断如果是a/b/…路径,则有自定义类加载器加载,不进行递归,即不委托给父加载器,其他路径的类还是要继续委托,因为任何类的父类都是Object,而Object必须由引导类加载器加载,所以其他jdk的核心类如Object类必须继续委托;
Tomcat就是打破了双亲委派机制,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

源码

java中类加载的几种方式

https://blog.csdn.net/weixin_31325725/article/details/114089889

https://blog.csdn.net/world_snow/article/details/99950881?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0.queryctr&spm=1001.2101.3001.4242.1&utm_relevant_index=3

触发类立即加载的几种情况

1 )遇到new ,getstatic,putstatic,invokestatic ,如果类没有初始化,就要先触发其初始化;
2 )如果java.lang.reflect包的方法对类进行反射调用时候,如果类没有进行过初始化,则需要优先触发其初始化;
3 )如果初始化一个类的时候,其父类还没有初始化,则需要优先触发其父类初始化
4 )当虚拟机启动时候,用户需要制定一个要执行的类(包含main()方法的类),虚拟机会优先触发这个主类;
5 )如果定义一个类的数组,这个类是不会进行初始化的,因为这个数组只有基本的属性,长度等;
6 )对于静态字段(非静态常量),只有直接定义这个字段的类,才会被初始化.因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化.至于是否要触发子类的加载和验证,在虚拟机中没有明确的 规定,这点取决于虚拟机的具体实现;.
7 )当一个类在初始化时,要求其父类全部已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正用到父接口时候,才会初始化

第一种情况就是new一个对象;或针对static但是非final变量,或读,或写;或者调用static方法;因为final修饰的变量,在代码编译阶段,会被放进常量池。

https://blog.csdn.net/xiangqianzou_liu/article/details/105441903?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-4&spm=1001.2101.3001.4242

https://www.cnblogs.com/liujinhong/p/6434141.html?utm_source=itdadao&utm_medium=referral

https://blog.csdn.net/mawei7510/article/details/83412304?utm_medium=distribute.pc_relevant_t0.none-task-blog-2defaultBlogCommendFromMachineLearnPai2default-1.withoutpai&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2defaultBlogCommendFromMachineLearnPai2default-1.withoutpai

普通内部类与静态内部类

1)内部类和静态内部类都是延时加载的,也就是说只有在明确用到内部类时才加载,只使用外部类时不加载。
2)非静态内部类不能拥有,静态变量,静态语句,静态方法
3)静态内部类无需外部类实例即可调用
4)非静态内部类需要外部类实例调用

类的初始化与实例化对象不同

在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。类的初始化属于类的生命周期的最后一个阶段,而实例化对象属于对象的生命周期;

程序加载过程

当执行java com.xxx.xxx.xxx.A.class时,运行的是类的字节码;
1)java.exe调用底层的jvm.dll文件创建jvm虚拟机(由c++实现);
2)创建一个引导类加载器实例,引导类加载器由c++实现;
3)创建jvm启动器sun.misc.Launcher实例,该类由引导类加载器加载,c++代码会调用getLauncher方法,其中又会执行Launcher的构造方法中,其中会实例化扩展类加载器和应用程序类加载器(以new的方式),并且将应用程序类加载器中的parent属性赋值为扩展类加载器;
4)获取运行类自己的类加载器,调用loadClass方法加载要运行的类Math.class从磁盘到jvm内存;
5)加载完成后,jvm会执行Math类的main方法;

上述第4)步就是我们经常讲到的类加载过程,包括加载,连接,初始化,使用,卸载;

举报

相关推荐

类的加载(中)

0 条评论