-
2018-07-28 17:21:07
Objects, Classes and ClassLoaders
对象(Objects),类(Classes)以及类加载器(ClassLoaders)
在Java中一切皆是对象(Object),并且所有对象都是由它们的类(Class)指定的。所以每一个对象都有一个到java.lang.Class(用于描述对象的结构)的实例的引用。
Person boss =
new
Person();
Java虚拟机(JVM)需要理解待创建对象的结构,为此,JVM会查找叫Person的类。并且,如果在程序的这一次运行中,Person类是第一次被访问的话,它必须要被JVM加载(loaded),通常这是从对应的Person.class文件加载的。从磁盘上查找Person.class文件、将其加载到内存中,并解析它的结构的过程,就叫作类的加载(class loading)。保证合适的类被加载是类加载器(ClassLoader)的职责。ClassLoader是java.lang.ClassLoader类的实例,并且Java程序中的每一个类都必须被一些ClassLoader加载,每一个classloader都持有它所加载的所有类的引用。
Java中一个实例对象被创建的过程
一、类的加载过程
首先,Jvm在执行时,遇到一个新的类时,会到内存中的方法区去找class的信息,如果找到就直接拿来用,如果没有找到,就会去将类文件加载到方法区。在类加载时,静态成员变量加载到方法区的静态区域,非静态成员变量加载到方法区的非静态区域。
静态代码块是在类加载时自动执行的代码,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。
加载过程:
1、JVM会先去方法区中找有没有相应类的.class存在。如果有,就直接使用;如果没有,则把相关类的.clss加载到方法区。
2、在.class加载到方法区时,先加载父类再加载子类;先加载静态内容,再加载非静态内容
3、加载静态内容:
- 把.class中的所有静态内容加载到方法区下的静态区域内
- 静态内容加载完成之后,对所有的静态变量进行默认初始化
- 所有的静态变量默认初始化完成之后,再进行显式初始化
- 当静态区域下的所有静态变量显式初始化完后,执行静态代码块
4、加载非静态内容:把.class中的所有非静态变量及非静态代码块加载到方法区下的非静态区域内。
5、执行完之后,整个类的加载就完成了。
对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,所以用户没有调用时都不执行,主要区别在于静态方法可以直接用类名直接调用(实例化对象也可以),而非静态方法只能先实例化对象后才能调用。
二、对象的创建过程
1、new一个对象时,在堆内存中开辟一块空间。
2、给开辟的空间分配一个地址。
3、把对象的所有非静态成员加载到所开辟的空间下。
4、所有的非静态成员加载完成之后,对所有非静态成员变量进行默认初始化。
5、所有非静态成员变量默认初始化完成之后,调用构造函数。
6、在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,
====①执行super()语句 ②对开辟空间下的所有非静态成员变量进行显示初始化 ③执行构造代码块====
再执行构造函数中书写的代码。
7、在整个构造函数执行完并弹栈后,把空间分配的地址赋给引用对象。
注: super语句,可能出现以下三种情况:
1)构造方法体的第一行是this()语句,则不会执行隐式三步,而是调用this()语句所对应的的构造方法,最终肯定会有第一行不是this语句的构造方法。
2)构造方法体的第一行是super()语句,则调用相应的父类的构造方法,
3)构造方法体的第一行既不是this()语句也不是super()语句,则隐式调用super(),即其父类的默认构造方法,这也是为什么一个父类通常要提供默认构造方法的原因。
Java的内存泄漏基本上按照内存区域的划分可以分为:
-
堆(heap)内存泄漏:大家都比较熟悉
-
栈(stack)内存泄漏:当前线程运行期间维护的中间变量等信息过多,例如常见的死循环引起stack over flow
- 方法区(permanent heap)内存泄漏:分析其原因的文章较少,本文的着重点。
运行时数据区分类
Java虚拟机的运行时数据区一般分类如下(不一定是物理划分):
- 堆:主要存放对象实例,线程共享
- 栈:主要存储特定线程的方法调用状态,线程独占
- 本地方法栈:存储本地方法的调用状态,线程独占
- PC寄存器:学过操作系统课程的都知道,线程独占
- 方法区:主要存储了类型信息,线程共享
方法区可以简单的等价为所谓的PermGen区域(永久存储区),在很多虚拟机相关的文档中,也将其称之为"永久堆"(permanent heap),作为堆空间的一部分存在。介于此,我们可以简单说明一下我们常用的几个堆内存配置的参数关系:
*-XX: PermSize:*永久堆(Pergen区域)大小默认值
*-XX:MaxPermSize:*永久堆(Pergen区域)最大值
*-Xms:*堆内存大小默认值
*-Xmx:*堆内存最大值运行时数据区访问方式总结
从开发者角度,虚拟机运行时数据区的访问方式简要归纳如下:
- 活动的线程可以通过对应的栈来访问运行时数据区信息
- 栈是堆访问的入口
- 堆上Java.lang.Class实例是访问PermGen区域中类型信息的入口
PermGen OOM原因总结
通过上面的测试程序分析,我们发现PermGen OOM发生的原因和类型装载、类型卸载有直接的关系,可以对PermGen OOM发生的原因做如下大致的总结:
- 为PermGen区域分配的堆空间过小,可以通过合理的设置-XX: PermSize参数和-XX:MaxPermSize参数来解决。
- 类型卸载不及时,过时无效的类型信息占用了空间,我们不妨称其为"永久堆"的内存泄漏,需要通过深入分析类型卸载的原理来寻找对应的防范措施。
常见的类加载器和类型卸载的可能性总结
通过前面的讨论,我们知道如果加载某种类型的类加载器实例没有处于unreachable状态,则该类型就不会被卸载,该类型不被卸载,则对应的类型信息在PermGen区域中占有的堆内存就不会被释放。下面,针对典型的Java应用分类,分析一下常用类加载器加载的类型被下载的可能性。
【普通Java应用】
系统类加载器:由于其负责加载虚拟机的核心类型,所以由其加载的类型在整个程序运行期间不可能被卸载,对应类型信息占用的PermGen区域堆空间不可能得到释放。
扩展类加载器:负责加载JDK扩展路径下的类型,扩展类加载器同时又作为系统类加载器的父类加载器,所以,由其加载的类型在整个程序运行期间基本上不可能被卸载,对应类型信息占用的PermGen区域堆空间基本不可能得到释放。
系统类加载器:负责加载程序类路径上面的类型,由其加载的类型在整个程序运行期间基本上不可能被卸载,对应类型信息占用的PermGen区域堆空间基本不可能得到释放。
用户自定义类加载器:对于其加载的类型,满足类型卸载要求的可能性比较容易控制,只要是其实例本身处于unreachable状态,其加载的类型会被卸载,PermGen区域中对应的空间占有也会被释放。【插件开发】
系统类加载器:由于其负责加载虚拟机的核心类型,所以由其加载的类型在插件应用运行期间不可能被卸载,对应类型信息占用的PermGen区域堆空间不可能得到释放。
插件类加载器:系统插件类加载器负责加载OSGI实现的相关类型,所以由其加载的类型在插件应用运行期间不可能被卸载;用户开发的插件所使用的默认插件类加载器,和特定的插件本身进行域绑定,插件之间存在一定的类型引用关系,并且特定插件在整个插件应用的运行时被停止的可能性也很小,所以类型卸载发生几率极小。
用户自定义类加载器:对于其加载的类型,满足类型卸载要求的可能性比较容易控制,只要是其实例本身处于unreachable状态,其加载的类型会被卸载,PermGen区域中对应的空间占有也会被释放。PermGen内存溢出的应对措施
通过上面的PermGen OOM的原因的分析,不难看出对应的应对措施:
- 合理的设置-XX: PermSize和-XX:MaxPermSize参数(主要的有效措施)
- 有效的利用的虚拟机类型卸载的机制(针对程序进行调优)
合理设置参数(针对普通用户和开发者)
通过设置合理的XX: PermSize和-XX:MaxPermSize参数值是减少和有效避免PermGen OOM发生的最有效最主要的措施,尤其是针对普通用户而言,这基本上是唯一的办法。关于合理设置这两个参数,建议如下:
- XX: PermSize参数的设置尽量建立在基准测试的基础之上,可以利用监控工具对稳定运行期间PermGen区域的大小进行统计,取合理的平均值。网上的很多资料中,建议XX: PermSize和XX:MaxPermSize设置为相同的数值,个人觉得这是不正确的,因为两个参数的出发点是不一样的。XX: PermSize设置的过大肯定会在应用运行的大部分时间中浪费堆内存,有可能会明显增加存放普通对象实例的堆空间的垃圾收集的次数。
- XX:MaxPermSize参数的设置应该着眼于PermGen区域使用的峰值,因为这是避免PermGen OOM的最后一道屏障,其设置最好也是建立在性能监控工具的统计结果之上。
- 和虚拟机有关的性能参数较多的分为两类,一类是初始值或默认值,一类是峰值。如果该性能参数是会涉及到的虚拟机垃圾收集机制的,关于初始值或者默认值的设置尽量要建立在测试基础之上,尽量做到在单次垃圾收集时间和垃圾收集频率之间保持一个平衡,否则很有可能适得其反。
有效利用虚拟机类型卸载机制(针对开发者)
此部分的建议可以作为开发者进行性能调优或者日常开发时候的参考,尽量能够配合相应的性能监控工具进行:
- 检查是否由于程序设计本身上的缺陷,导致加载了大量实际上并不需要的类型。较新版本的Java虚拟机实现,一般都遵循动态解析的建议,所以不是人为设计的缺陷,一般不会诱发加载了大量实际上并不需要的类型。结合插件开发的应用场景,个人觉得插件功能模块的划分(其中包括了插件依赖关系的设计和有关扩展点的扩展收集等)和第三方jar的使用可能是诱发此问题的两个重要根源。
- 对象缓存的使用是否得当,通过前面的分析,我们知道这可能是导致类型不能被卸载的重要原因。缓存的使用,既要认识到其可以提高时间性能的有点,也要分析其可能会给普通对象堆空间和PermGen区域造成的负担。
- 自定义类加载器的合理使用,相关的几个注意要点包括:
- 是否不恰当的利用的类型更新的特性,也就是说是否对类加载器实例的unreachable状态做了有效的判断。考虑如下场景,假设用户开发了一个自定义类加载器来加载工程输出目录下的临时类型,对临时类型做了不必要的缓存,这肯定会导致所有被加载过的临时类型都不会得到卸载,会直接加重PermGen区域的负担。
- 自定义类加载器和其他已有类加载器的协作关系是否合理,是否合理的利用了Java类加载的双亲委派机制。我们知道,不同的类加载器实例(哪怕是同一种类加载器类型的不同实例)加载的同一种自定义类型在虚拟机内部都会被放置到不同的命名空间中作为不同类型来处理,所以合理的设置父类加载器变得很重要,不合理的设置会导致大量不必要的"新"类型被创造出来,况且这些不必要的"新"类型是否能够被及时卸载还是个未知数。
- 慎重检查自定义类加载器实例是否被不恰当的缓存了,原因不言而喻。
转载自:
更多相关内容 -
深入理解Java对象的创建过程:类的初始化与实例化
2017-05-18 14:17:45在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。...本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖一个Java对象的创建过程。摘要:
在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖一个Java对象的创建过程。
版权声明:
本文原创作者:书呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/
友情提示:
一个Java对象的创建过程往往包括 类初始化 和 类实例化 两个阶段。本文的姊妹篇《 JVM类加载机制概述:加载时机与加载过程》主要介绍了类的初始化时机和初始化过程,本文在此基础上,进一步阐述了一个Java对象创建的真实过程。
一、Java对象创建时机
我们知道,一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象,这种方式在Java规范中被称为 : 由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。下面笔者分别对此进行一一介绍:
1). 使用new关键字创建对象
这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。比如:
Student student = new Student();
2). 使用Class类的newInstance方法(反射机制)
我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象,比如:
Student student2 = (Student)Class.forName("Student类全限定名").newInstance(); 或者: Student stu = Student.class.newInstance();
3). 使用Constructor类的newInstance方法(反射机制)
java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象,该方法和Class类中的newInstance方法很像,但是相比之下,Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数,比如:
public class Student { private int id; public Student(Integer id) { this.id = id; } public static void main(String[] args) throws Exception { Constructor<Student> constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); } }
使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。
4). 使用Clone方法创建对象
无论何时我们调用一个对象的clone方法,JVM都会帮我们创建一个新的、一样的对象,特别需要说明的是,用clone方法创建对象的过程中并不会调用任何构造函数。关于如何使用clone方法以及浅克隆/深克隆机制,笔者已经在博文《 Java String 综述(下篇)》做了详细的说明。简单而言,要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法,这也是原型模式的应用。比如:
public class Student implements Cloneable{ private int id; public Student(Integer id) { this.id = id; } @Override protected Object clone() throws CloneNotSupportedException { // TODO Auto-generated method stub return super.clone(); } public static void main(String[] args) throws Exception { Constructor<Student> constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); Student stu4 = (Student) stu3.clone(); } }
5). 使用(反)序列化机制创建对象
当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口,比如:
public class Student implements Cloneable, Serializable { private int id; public Student(Integer id) { this.id = id; } @Override public String toString() { return "Student [id=" + id + "]"; } public static void main(String[] args) throws Exception { Constructor<Student> constructor = Student.class .getConstructor(Integer.class); Student stu3 = constructor.newInstance(123); // 写对象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("student.bin")); output.writeObject(stu3); output.close(); // 读对象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "student.bin")); Student stu5 = (Student) input.readObject(); System.out.println(stu5); } }
6). 完整实例
public class Student implements Cloneable, Serializable { private int id; public Student() { } public Student(Integer id) { this.id = id; } @Override protected Object clone() throws CloneNotSupportedException { // TODO Auto-generated method stub return super.clone(); } @Override public String toString() { return "Student [id=" + id + "]"; } public static void main(String[] args) throws Exception { System.out.println("使用new关键字创建对象:"); Student stu1 = new Student(123); System.out.println(stu1); System.out.println("\n---------------------------\n"); System.out.println("使用Class类的newInstance方法创建对象:"); Student stu2 = Student.class.newInstance(); //对应类必须具有无参构造方法,且只有这一种创建方式 System.out.println(stu2); System.out.println("\n---------------------------\n"); System.out.println("使用Constructor类的newInstance方法创建对象:"); Constructor<Student> constructor = Student.class .getConstructor(Integer.class); // 调用有参构造方法 Student stu3 = constructor.newInstance(123); System.out.println(stu3); System.out.println("\n---------------------------\n"); System.out.println("使用Clone方法创建对象:"); Student stu4 = (Student) stu3.clone(); System.out.println(stu4); System.out.println("\n---------------------------\n"); System.out.println("使用(反)序列化机制创建对象:"); // 写对象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("student.bin")); output.writeObject(stu4); output.close(); // 读取对象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "student.bin")); Student stu5 = (Student) input.readObject(); System.out.println(stu5); } }/* Output: 使用new关键字创建对象: Student [id=123] --------------------------- 使用Class类的newInstance方法创建对象: Student [id=0] --------------------------- 使用Constructor类的newInstance方法创建对象: Student [id=123] --------------------------- 使用Clone方法创建对象: Student [id=123] --------------------------- 使用(反)序列化机制创建对象: Student [id=123] *///:~
从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。
二. Java 对象的创建过程
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
1、实例变量初始化与实例代码块初始化
我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。例如:
public class InstanceVariableInitializer { private int i = 1; private int j = i + 1; public InstanceVariableInitializer(int var){ System.out.println(i); System.out.println(j); this.i = var; System.out.println(i); System.out.println(j); } { // 实例代码块 j += 3; } public static void main(String[] args) { new InstanceVariableInitializer(8); } }/* Output: 1 5 8 5 *///:~
上面的例子正好印证了上面的结论。特别需要注意的是,Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量,比如:
public class InstanceInitializer { { j = i; } private int i = 1; private int j; } public class InstanceInitializer { private int j = i; private int i = 1; }
上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如:
public class InstanceInitializer { private int j = getI(); private int i = 1; public InstanceInitializer() { i = 2; } private int getI() { return i; } public static void main(String[] args) { InstanceInitializer ii = new InstanceInitializer(); System.out.println(ii.j); } }
如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,这一动作发生在实例变量i初始化之前和构造函数调用之前。
2、构造函数初始化
我们可以从上文知道,实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。
我们知道,Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用,比如:
public class ConstructorExample { }
对于上面代码中定义的类,我们观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下,
aload_0 invokespecial #8; //Method java/lang/Object."<init>":()V return
上面代码的第二行就是调用Object类的默认构造函数的指令。也就是说,如果我们显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面,也就是必须是构造函数的第一条指令。正因为如此,Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。
特别地,如果我们在一个构造函数中调用另外一个构造函数,如下所示,
public class ConstructorExample { private int i; ConstructorExample() { this(1); .... } ConstructorExample(int i) { .... this.i = i; .... } }
对于这种情况,Java只允许在ConstructorExample(int i)内调用超类的构造函数,也就是说,下面两种情形的代码编译是无法通过的:
public class ConstructorExample { private int i; ConstructorExample() { super(); this(1); // Error:Constructor call must be the first statement in a constructor .... } ConstructorExample(int i) { .... this.i = i; .... } }
或者,
public class ConstructorExample { private int i; ConstructorExample() { this(1); super(); //Error: Constructor call must be the first statement in a constructor .... } ConstructorExample(int i) { this.i = i; } }
Java通过对构造函数作出这种限制以便保证一个类的实例能够在被使用之前正确地初始化。
3、 小结
总而言之,实例化一个类的对象的过程是一个典型的递归过程,如下图所示。进一步地说,在实例化一个类的对象时,具体过程是这样的:
在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
Ps: 关于递归的思想与内涵的介绍,请参见我的博文《 算法设计方法:递归的内涵与经典应用》。
4、实例变量初始化、实例代码块初始化以及构造函数初始化综合实例
笔者在《 JVM类加载机制概述:加载时机与加载过程》一文中详细阐述了类初始化时机和初始化过程,并在文章的最后留了一个悬念给各位,这里来揭开这个悬念。建议读者先看完《 JVM类加载机制概述:加载时机与加载过程》这篇再来看这个,印象会比较深刻,如若不然,也没什么关系~~
//父类 class Foo { int i = 1; Foo() { System.out.println(i); -----------(1) int x = getValue(); System.out.println(x); -----------(2) } { i = 2; } protected int getValue() { return i; } } //子类 class Bar extends Foo { int j = 1; Bar() { j = 2; } { j = 3; } @Override protected int getValue() { return j; } } public class ConstructorExample { public static void main(String... args) { Bar bar = new Bar(); System.out.println(bar.getValue()); -----------(3) } }/* Output: 2 0 2 *///:~
根据上文所述的类实例化过程,我们可以将Foo类的构造函数和Bar类的构造函数等价地分别变为如下形式:
//Foo类构造函数的等价变换: Foo() { i = 1; i = 2; System.out.println(i); int x = getValue(); System.out.println(x); }
//Bar类构造函数的等价变换 Bar() { Foo(); j = 1; j = 3; j = 2 }
这样程序就好看多了,我们一眼就可以观察出程序的输出结果。在通过使用Bar类的构造方法new一个Bar类的实例时,首先会调用Foo类构造函数,因此(1)处输出是2,这从Foo类构造函数的等价变换中可以直接看出。(2)处输出是0,为什么呢?因为在执行Foo的构造函数的过程中,由于Bar重载了Foo中的getValue方法,所以根据Java的多态特性可以知道,其调用的getValue方法是被Bar重载的那个getValue方法。但由于这时Bar的构造函数还没有被执行,因此此时j的值还是默认值0,因此(2)处输出是0。最后,在执行(3)处的代码时,由于bar对象已经创建完成,所以此时再访问j的值时,就得到了其初始化后的值2,这一点可以从Bar类构造函数的等价变换中直接看出。
三. 类的初始化时机与过程
关于类的初始化时机,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》已经介绍的很清楚了,此处不再赘述。简单地说,在类加载过程中,准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,而初始化阶段是真正开始执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程。更直接地说,初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。
类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来说并不是必需的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。此外,在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用多次,只要程序员还在创建对象。
注意,这里所谓的实例构造器<init>()是指收集类中的所有实例变量的赋值动作、实例代码块和构造函数合并产生的,类似于上文对Foo类的构造函数和Bar类的构造函数做的等价变换。
四. 总结
1、一个实例变量在对象初始化的过程中会被赋值几次?
我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。
2、类的初始化过程与类的实例化过程的异同?
类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类完全加载到内存中后创建对象的过程。
3、假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?
我们知道,要想创建一个类的实例,必须先将该类加载到内存并进行初始化,也就是说,类初始化操作是在类实例化操作之前进行的,但并不意味着:只有类初始化操作结束后才能进行类实例化操作。例如,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》中所提到的下面这个经典案例:
public class StaticTest { public static void main(String[] args) { staticFunction(); } static StaticTest st = new StaticTest(); static { //静态代码块 System.out.println("1"); } { // 实例代码块 System.out.println("2"); } StaticTest() { // 实例构造器 System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void staticFunction() { // 静态方法 System.out.println("4"); } int a = 110; // 实例变量 static int b = 112; // 静态变量 }/* Output: 2 3 a=110,b=0 1 4 *///:~
大家能得到正确答案吗?笔者已经在博文《 JVM类加载机制概述:加载时机与加载过程》中解释过这个问题了,此不赘述。
总的来说,类实例化的一般过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。
五. 更多
更多关于类初始化时机和初始化过程的介绍,请参见我的博文《 JVM类加载机制概述:加载时机与加载过程》。
更多关于类加载器等方面的内容,包括JVM预定义的类加载器、双亲委派模型等知识点,请参见我的转载博文《深入理解Java类加载器(一):Java类加载原理解析》。
关于递归的思想与内涵的介绍,请参见我的博文《 算法设计方法:递归的内涵与经典应用》。
引用:
-
创建(实例化)对象的五种方式
2018-08-28 10:16:52Java中创建(实例化)对象的五种方式 用new语句创建对象,这是最常见的创建对象的方法。 通过工厂方法返回对象,如:String str = String.valueOf(23); 运用反射手段,调用java.lang.Class或者java.lang....目录
一、Java中创建(实例化)对象的五种方式
- 用new语句创建对象,这是最常见的创建对象的方法;
- 调用对象的clone()方法;
- 运用反射手段,调用java.lang.Class或者java.lang。 reflect.Constructor类的newInstance()实例方法。如:Object obj = Class.forName(“java.lang.Object”)。newInstance();
- 通过I / O流(包括反序列化),如运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法;
- 通过工厂方法返回对象,如:String str = String.valueOf(23);
1、new关键字创建对象; 2、调用对象的clone()方法创建对象
/** * @ClassName Bigwig * @Description new关键字创建对象; * 调用对象的clone()方法创建对象 * 测试Cloneable接口的使用 * 实例化对象 * @Author lzq * @Date 2019/6/15 19:53 * @Version 1.0 **/ public class Bigwig implements Cloneable{ private String name; private int age; public Bigwig(String name,int age) { this.name = name; this.age = age; } public String getName () { return name; } public int getAge () { return age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { String s = "姓名是:"+this.name+"\t年龄是:"+this.age; return s; } }
public static void main(String[] args) throws CloneNotSupportedException { Bigwig p1 = new Bigwig("诸葛亮", 30); //new关键字创建的对象 System.out.println(p1); Bigwig p2 = null; p2 = (Bigwig) p1.clone(); 调用对象的clone()方法创建对象 System.out.println(p2); p2.setAge(51); p2.setName("司马懿"); System.out.println(p2); }
运行结果:
姓名是:诸葛亮 年龄是:30 姓名是:诸葛亮 年龄是:30 姓名是:司马懿 年龄是:51
3、通过反射对对象进行初始化
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /* * 通过反射对对象进行初始化 * 注意必须有无参数的Constructor * 实例化Class类然后调用newInstance()方法 * */ class Student{ private Integer age; private String name; public Student() { System.out.println("无参构造"); } public Student(String name) { System.out.println("一参构造"); } public Student(String name,Integer age) { System.out.println("双参构造"); } } public class TestClass { public static void main(String[] args) { try{ Class clazz = Class.forName("com.xagy.lianxi.Student"); /** * 调用无参构造函数, * 效果上和使用class.newInstance()差不多 * 构造函数无参的情况下,可以传入一个空数组,也可以不传入数组 */ Constructor<Student> con = clazz.getConstructor(); Student thisIsTestClass = con.newInstance(); System.out.println("-------------------------------------"); //依然是无参构造函数 Class[] paramTypeEmpty = {}; Constructor<Student> con0 = clazz.getConstructor(paramTypeEmpty); Object[] paramEmpty = {}; Student thisIsTestClassEmpty = con0.newInstance(paramEmpty); System.out.println("-------------------------------------"); //getConstructor接受变长参数,以Class数组形式传入, //告诉jdk我们要调用哪个构造器 Class[] paramType1 = {String.class}; Constructor<Student> con1 = clazz.getConstructor(paramType1); //Constructor实例的newInstance同样也是接受一个变长参数, //参数数组中类型要和getConstructor的类型对应 Object[] params1 = {"ParamOne"}; Student thisIsTestClass1 = con1.newInstance(params1); System.out.println("-------------------------------------"); //params2中的数据类型要和paramTypes2中的类型相对应 Class[] paramTypes2 = {String.class,Integer.class}; Constructor<Student> con2 = clazz.getConstructor(paramTypes2); Object[] params2 = {"ParamOne",2}; Student thisIsTestClass2 = con2.newInstance(params2); System.out.println("-------------------------------------"); }catch(ClassNotFoundException e){ e.printStackTrace(); }catch (NoSuchMethodException e){ e.printStackTrace(); }catch (SecurityException e){ e.printStackTrace(); }catch (InstantiationException e){ e.printStackTrace(); }catch (IllegalAccessException e){ e.printStackTrace(); }catch (IllegalArgumentException e){ e.printStackTrace(); }catch (InvocationTargetException e){ e.printStackTrace(); } } }
运行结果:
无参构造 ------------------------------------- 无参构造 ------------------------------------- 一参构造 ------------------------------------- 双参构造 -------------------------------------
4、序列化
①、序列化是干什么的?
简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自 己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。②、什么情况下需要序列化
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;③、相关注意事项
a)序列化时,只对对象的状态进行保存,而不管对象的方法;
b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
d)并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:
Ⅰ.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
Ⅱ. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现。import java.io.*; /** * @ClassName TestDemo7 * @Description 对象序列化 * @Author lzq * @Date 2018/12/11 11:36 * @Version 1.0 **/ class Student implements Serializable { private static final long serialVersionUID = -88175599799432325L; private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String toString() { return "name=" + name + ", age=" + age; } } public class TestDemo7 { public static void main(String[] args) { String filename = "F:/FHB/serialize.txt"; serialize(filename); reverse_serialize(filename); } /** * 序列化 */ public static void serialize(String filename) { try { OutputStream fos = new FileOutputStream(filename); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeInt(12345); oos.writeObject("Today"); oos.writeObject(new Student("lzq",20)); oos.close(); fos.close(); }catch (Exception e) { e.printStackTrace(); } } /** * 反序列化 */ public static void reverse_serialize(String filename) { try { InputStream in = new FileInputStream(filename); ObjectInputStream obj = new ObjectInputStream(in); int i = obj.readInt(); String str = (String)obj.readObject(); Student student = (Student)obj.readObject(); System.out.println(i); System.out.println(str); System.out.println(student); obj.close(); in.close(); }catch (Exception e) { e.printStackTrace(); } } }
12345 Today name=lzq, age=20
5、通过工厂方法返回对象
二、Java中,类的实例化方法有四种途径:
1)使用new关键字
2)利用反射,调用Class对象的newInstance()方法
3)调用clone()方法,对现有实例的拷贝
4)利用反序列化,通过ObjectInputStream的readObject()方法反序列化类
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; class ClassInstance implements Cloneable, Serializable { private String str = "测试..."; public void fun(){ System.out.println(str); } public ClassInstance(String str) { System.out.println("有参类的实例化"); this.str += str; } public ClassInstance() { System.out.println("无参类的实例化"); } public Object clone() { return this; } } public class ClassInstanceTest{ public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException,InvocationTargetException, NoSuchMethodException{ //第一种类的实例化方式 ClassInstance ci01 = new ClassInstance("01"); ci01.fun(); //第二种类的实例化方式 ClassInstance ci02 = (ClassInstance) Class.forName("com.xagy.lianxi.ClassInstance").newInstance(); ci02.fun(); //第三种类的实例化方式 ClassInstance ci03 = (ClassInstance) ci01.clone(); ci03.fun(); //第四种类的实例化方式 FileOutputStream fos = new FileOutputStream("D:\\ci.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(ci01); oos.close(); fos.close(); FileInputStream fis = new FileInputStream("D:\\ci.txt"); ObjectInputStream ois = new ObjectInputStream(fis); ClassInstance ci04 = (ClassInstance) ois.readObject(); ois.close(); fis.close(); ci04.fun(); System.out.println("--------------------额外测试--------------------"); ClassInstance ci05 = null; //额外的思考 在第二种类实例化的方式中有没有一种方法实现有参数的构造方式 //获得类的构造信息 Constructor[] ctor = Class.forName("com.xagy.lianxi.ClassInstance").getDeclaredConstructors(); //找到我们需要的构造方法 for(int i=0;i<ctor.length;i++ ){ Class[] cl = ctor[i].getParameterTypes(); if(cl.length == 1){ //实例化对象 ci05 = (ClassInstance) Class.forName("com.xagy.lianxi.ClassInstance").getConstructor(cl).newInstance(new Object[]{"05"}); } } ci05.fun(); } }
有参类的实例化 测试...01 无参类的实例化 测试... 测试...01 测试...01 --------------------额外测试-------------------- 有参类的实例化 测试...05
-
Unity 创建/实例化对象
2021-09-23 11:40:30在程序运行过程中创建、实例化对象物体需要用到Object类中的 Instantiate 函数,例如,我们场景中有一个物体A: 现在我们想要在场景中创建五个该物体,则用Instantiate函数将该物体作为参数传入: using ...在程序运行过程中创建、实例化对象物体需要用到Object类中的 Instantiate 函数,例如,我们场景中有一个物体A:
现在我们想要在场景中创建五个该物体,则用Instantiate函数将该物体作为参数传入:
using UnityEngine; public class Foo : MonoBehaviour { public GameObject A; private void Start() { for (int i = 0; i < 5; i++) { Instantiate(A); } } }
运行效果:
场景中多出的五个A(Clone)物体即为我们创建的物体。
-
详解JAVA对象实例化过程
2020-08-18 10:32:171 对象的实例化过程对象的实例化过程是分成两部分:类的加载初始化,对象的初始化要创建类的对象实例需要先加载并初始化该类,main方法所在的类需要先加载和初始化类初始化就是执行<clinit>方法,对象实例化是... -
Java对象创建过程
2022-03-28 17:54:13java对象创建过程、对象的组成、对象头、实例数据、对齐填充、对象创建方式、new关键字、Class类的newInstance方法、Constructor类的newInstance方法、Object类的clone方法、反序列化、无父类的对象创建、有父类的... -
为什么要创建对象(实例化)?
2020-06-04 19:55:49一、为什么要实例化对象? 对象:属性+方法 类是指:描述一种事物的定义,是个抽象的概念 实例指:该种事物的一个具体的个体,是具体的东西 联系 类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及... -
Java类加载及对象创建过程详解
2019-06-27 08:00:00类加载过程 类加载的五个过程:加载、验证、准备、解析、初始化。 ... 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。 验证 验证阶段作用是保证Class... -
NO.5 Spring IOC 对象的实例化过程
2017-12-12 17:36:21NO.5 Spring IOC 对象的实例化过程 这里,我们需要知道一个名词——DI(依赖注入)。 需要一个对象,就让工厂注入进去一个对象,需要一个service,就让IOC注入一个service。 IOC就是一个大工厂,专门用来生产对象... -
JVM之内存和对象创建过程
2018-08-29 14:32:10优势:在对象被移动(GC中移动对象很常见)时只会改变句柄的实例数据指针,reference本身不需要修改 2.直接指针:堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的直接是对象... -
Java反射中的创建对象和对象实例化有什么区别
2018-09-25 11:33:12在学习Java反射的过程中,遇到了一点疑惑的地方 //1.... 利用Class对象的newInstance方法创建一个类的实例 Object obj = clazz.newInstance(); 平常理解的对象和实例都是一个东西,将一个对象实... -
Java对象在内存中实例化的过程
2020-07-27 15:08:52Java对象在内存中实例化的过程 在讲 Java 对象在内存中的实例化过程前,先来说下在类的实例化过程中,内存会使用到的三个区域:栈区、堆区、方法区。 堆区: 存储的全部都是对象,每个对象包含了一个与之对应的 ... -
对C#中用new关键字实例化对象的理解
2019-12-27 16:27:50【前置知识】 对值类型和引用类型的理解 公共语言运行时CLR ...实例化是从类创建对象的过程。 举例而言:类可以看做是图纸,我们根据这个图纸做出来一个东西(即对象),做出来的东西就是图... -
java动态代理对象实例的创建
2016-02-02 11:58:00 -
(十)vue实例对象介绍
2018-11-12 13:27:47前三个基本上已经讲了,我们直接来看实例对象的计算属性 (4)Computed 计算属性 1)什么是计算属性 1.计算属性即computed, 和前面我们讲的methods方法基本上一样。他们的写法都是函数形式。 2.不同的是,methods... -
声明对象和实例化对象的区别
2019-05-31 20:40:45声明对象: 和声明基本引用类型是一样的, 类名 对象名(对象名也是标识符,和基本类型的...如此处理,使得Java中声明一个对象的消耗很小,但也有一个副作用,就是对象不能马上使用,还需要对它进行实例化。 实... -
JDK1.8——Java对象的创建过程
2020-08-16 20:22:20Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。 对象创建过程概述 对象的创建过程如图: 这里解释一下什么是符号引用: 符号引用: 符号引用是一个字符串,它给出了被引用的内容... -
面试题——Java 类加载/创建对象的过程
2019-11-06 20:13:52在堆区为实例对象分配内存2. 对实例变量赋默认值3. 执行实例初始化代码4. 将堆区对象的地址赋值给栈区的引用变量流程图 面试题——new一个对象/创建对象的过程 过程:第一步类加载和初始化(第一次使用该类),第二步... -
linux手动创建oracle实例全过程
2017-11-28 18:22:37先理解几个概念 oracle跟mysql和mssql的不同,提出了实例和表空间等的概念 实例:即一个运行的服务,不含任何物理数据和内容 ...因此,想要创建一个新的数据库,必须先运行一个实例。 但是oracle有表 -
Java中创建对象的六个步骤 细分后(new关键字)对象头详细介绍
2020-07-16 22:31:39要看的懂对象的创建过程,首先你得有对Java虚拟机和Java基础以及JUC很是熟悉,比如类的加载过程,CAS、多线程、JVM的GC等等 首先好看一个图,我大概会根据图中的内容来分享这六个步骤(其实几个步骤不重要,只要包括... -
new创建对象的过程
2018-07-16 08:21:31使用new关键字调用函数(var p=new ClassA( ))的具体步骤:1. 创建空对象;var p= {};2. 设置新对象的__proto__...3. 使用新对象调用函数,函数中的this被指向新实例对象: ClassA.call(p); //{}.构造函数(); 4... -
创建一个构造函数实例的过程
2019-06-20 23:43:41调用构造函数实际上会经历4个步骤: ...1、创建一个新对象 2、将构造函数的作用域赋值给新对象(因此this就指向了新对象) 3、运行构造函数里面的代码(为新对象添加属性或方法) 4、返回新对象 ... -
【面向对象编程】(1) 类实例化的基本方法
2022-01-02 12:38:16各位同学好,本章节和大家分享一下面向对象编程的一些方法,通过一些案例带大家由浅入深掌握面向对象的编程。...item1 = Item() # 创建Item类的实例化对象item1 #(3)给实例化对象分配属性 item1.n.. -
C++类对象的创建与释放过程
2019-08-16 16:58:18C++类对象的创建与释放过程类对象的创建过程类对象的释放过程析构函数缺省析构函数 类对象的创建过程 1、分配类所需要的空间,无论是栈还是堆。 2、传递实参调用构造函数,完成如下任务: 1、根据继承表依次调用父类... -
子类对象的创建过程
2016-03-16 20:50:39Java对象构造过程 类初始化的时机 初始化类时的操作 -
java new对象的创建过程
2018-07-29 07:00:54关于对象的创建过程一般是从new指令(我说的是JVM的层面)开始的(具体请看图1),JVM首先对符号引用进行解析,如果找不到对应的符号引用,那么这个类还没有被加载,因此JVM便会进行类加载过程(具体加载过程可参见我的... -
C++ 对象和实例的区别,以及用new和不用new创建类对象区别
2015-04-06 21:44:20new创建类对象,使用完后需使用delete删除,跟申请内存类似。所以,new有时候又不太适合,比如在频繁调用场合,使用局部new类对象就不是个好选择,使用全局类对象或一个经过初始化的全局类指针似乎更加高 -
JAVA反射机制分析-------spring的通过反射创建bean实例对象以及属性注入的原理解析
2019-01-17 19:45:04对于任意一个对象,都能够调用他的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为JAVA语言的反射机制。 巧妙的利用java中的反射机制,能够帮助我们进行程序开发时达到意想不到... -
Spring3.1.0实现原理分析(九).AOP创建代理对象的过程
2017-05-18 10:49:48大家好,今天我会用一个例子来讲解Spring创建bean代理对象的过程,为大家揭开Spring AOP的神秘面纱。在看这篇博客前我强烈建议读者先看下这两篇博客《Spring3.1.0实现原理分析(六).实例化》,《Spring3.1.0实现原理...