<Java><类加载机制><反射>
类加载过程
- 类从被加载到虚拟机内存开始,直到卸载出内存,它的整个生命周期包括:加载(Loading), 验证(Verification), 准备(Preparation), 解析(Resolution), 初始化(Initialization), 使用(Using)和卸载(Unloading) 7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如下图:
- 其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,但解析阶段则不一定。在某些情况下解析可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)。
加载
- 在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需完成以下3件事情:
- 通过一个类的全限定名来获取此类的二进制字节流(并未指明要从一个Class文件获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
验证
- 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行
- 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
- 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。)
- 其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。
解析
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
- 这里讲到的符号引用和直接引用:
- 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
初始化
- 类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。
- 初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。比如:
public class Test
{
static
{
i=0;
System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
}
static int i=1;
}
- <clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<init>()方法执行之前,父类的<clinit>()方法方法已经执行完毕
- 类初始化的场景:
- new指令:使用new关键字实例化对象时;
- getstatic/putstatic指令:读取或设置一个类的静态字段时(被final修饰、已在编译期把结果放入常量池的静态字段除外);【原因:常量是一种特殊的变量,编译器将它们当做值而不是域来对待。编译器会在编译时将常量的值插到字节码中。这是一种很有用的优化。】
- invokestatic指令:调用一个类的静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用时,若类还未进行过初始化,则需要先触发其初始化。
- 当初始化一个类时,发现其父类还未初始化,则需先触发父类的初始化。
- 当虚拟机启动时,用户需要一个执行的主类(main()方法所在的类),虚拟机会先初始化该类。
- 除此之外,还有一些不会触发初始化的例子:
- 子类调用父类的静态变量,子类不会被初始化,只有父类被初始化。也即,对于静态变量,只有直接定义该字段的类会被初始化。
- 通过数组定义来引用类,不会触发初始化。
- 访问类的常量,不会初始化类。
看下面的例子
class SuperClass {
static {
System.out.println("superclass init");
}
public static int value = 123;
} class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
} public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);// 调用父类的静态变量,只初始化父类
SubClass[] sca = new SubClass[10];// 用数组定义来引用类,SubClass未被初始化
// 程序输出: superclass init 123
}
}
class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
} public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);// 调用类常量,ConstClass未被初始化
// 程序输出: hello world
}
}
实例分析
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0; private SingleTon() {
count1++;
count2++;
} public static SingleTon getInstance() {
return singleTon;
}
} public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
上述程序的输出是 1, 0而不是1, 1。为什么呢?我们分析一下上述程序的过程:
- 首先类加载、验证,然后在准备阶段为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0;
- 然后singleTon.getInstance()触发了类的初始化,为类的静态变量赋值并执行静态代码块。
- 首先,执行new SingleTon()方法,之后又count1 = 1, count2 = 1;
- 然后继续为count1和count2赋值,此时count1没有赋值操作,所有count1为1,而count2被赋值为0。
反射
- 反射机制允许程序在执行过程中,利用Reflection API取得任何已知名称的类的内部信息,包括:package、type parameters、superclass、implemented interfaces、inner classes、outer classes、fileds、constructors、methods、modifiers等,并在执行过程中,动态生成instances、变更fileds内容或唤起methods。
- 获取构造方法:
- Constructor getConstructor(Class[] params) --> 根据构造函数的参数,返回一个具体的具有public属性的构造函数;
- Constructor getConstructors() --> 返回所有具有public属性的构造函数数组
- Constructor getDeclaredConstructor(Class[] params) --> 根据构造函数的参数,返回一个具体的构造函数(不分public和非public属性)
- Constructor getDeclaredConstructors() --> 返回该类中所有的构造函数数组(不分public和非public属性)
- 获取类的成员方法:也是4种,跟获取构造方法类似。
- 获取类的成员变量:同上。
- 获取构造方法:
反射机制能做什么
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理。
System.out.println(tt.getClass().getName()); // 通过一个对象获得完整的包名和类名
Class<?> class1 =
null
;
Class<?> class2 =
null
;
Class<?> class3 =
null
;
// 一般采用这种形式
class1 = Class.forName(
"net.xsoftlab.baike.TestReflect"
);
class2 =
new
TestReflect().getClass();
class3 = TestReflect.
class
;
System.out.println(
"类名称 "
+ class1.getName());
System.out.println(
"类名称 "
+ class2.getName());
System.out.println(
"类名称 "
+ class3.getName());
<Java><类加载机制><反射>的更多相关文章
- 简单物联网:外网访问内网路由器下树莓派Flask服务器
最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...
- 利用ssh反向代理以及autossh实现从外网连接内网服务器
前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...
- 外网访问内网Docker容器
外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...
- 外网访问内网SpringBoot
外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...
- 外网访问内网Elasticsearch WEB
外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...
- 怎样从外网访问内网Rails
外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...
- 怎样从外网访问内网Memcached数据库
外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...
- 怎样从外网访问内网CouchDB数据库
外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...
- 怎样从外网访问内网DB2数据库
外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...
- 怎样从外网访问内网OpenLDAP数据库
外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...
随机推荐
- Android--------内存泄露工具LeakCanary
什么是内存泄露 一些对象有着有限的生命周期.当这些对象所要做的事情完成了,我们希望他们会被回收掉.但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收 ...
- VS Code行内样式提示插件
打开vscode,在软件界面左下角找到“齿轮”标志并点击,在弹出的菜单中选择“设置”,把下面的代码添加到设置里. { "workbench.colorTheme": "C ...
- Python while循环实现重试
try: pass#要执行的代码 except: 状态=True while 状态==True: try: winsound.Beep(800, 1000)#报警提示音 循环=300 while 循环 ...
- 第一阶段——站立会议总结DAY09
1.昨天做了什么:未做. 2.今天准备做什么:准备将之前讲的东西,要付诸实践.所以,为了使界面更加耐看,向微信,QQ这样的看齐,查一查个人中心界面中间的条条框框的实现代码,借鉴,并运用到自己的代码上. ...
- C++ string的用法和例子
使用场合: string是C++标准库的一个重要的部分,主要用于字符串处理.可以使用输入输出流方式直接进行操作,也可以通过文件等手段进行操作.同时C++的算法库对string也有着很好的支持,而且st ...
- jackson实体转json时 为NULL不参加序列化的汇总
首先加入依赖 <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson ...
- 《高性能SQL调优精要与案例解析》一书谈SQL调优(SQL TUNING或SQL优化)学习
<高性能SQL调优精要与案例解析>一书上市发售以来,很多热心读者就该书内容及一些具体问题提出了疑问,因读者众多外加本人日常工作的繁忙 ,在这里就SQL调优学习进行讨论并对热点问题统一作答. ...
- elasticsearch设置外部可访问
修改/config/elasticsearch.yml文件,增加如下配置: network.host: 0.0.0.0 浏览器访问http://192.168.17.134:9200/效果: 实际操作 ...
- ubuntu默认启动方式修改 psensor命令
Check UUID sudo blkid Then sudo gedit /etc/default/grub & to pull up the boot loader configurati ...
- Java Web(九) JDBC及数据库连接池及DBCP,c3p0,dbutils的使用
DBCP.C3P0.DBUtils的jar包和配置文件(百度云盘):点我下载 JDBC JDBC(Java 数据库连接,Java Database Connectify)是标准的Java访问数据库的A ...