深入Java虚拟机第七章读书笔记
一旦一个类被装载、连接和初始化,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。
类实例化
类实例化有四种方式:
1、明确地使用new操作符
2、调用Class或者java.lang.Constructor对象的newInstance()方法
3、调用任何现有对象的clone()方法
4、通过java.io.lang.ObjectInputStream类的getObject()方法反序列化
另外还有几种情况会被隐含地创建
1、在任何Java程序中第一个隐含实例化对象可能就是保存命令行参数的String对象。
2、在Java虚拟机装载的每一个类型,它会实例化一个Class对象来代表这个类型。
3、当Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象的实例来表示这些常量字符串。
4、还有一个隐含创建对象的途径就是通过执行包含字符串连接操作符的表达式产生的对象。
当Java虚拟机创建一个类的实例时,不管是明确的函数隐含的,首先需要在堆中为对象实例分配内存。一旦虚拟机为新的对象准备好堆内存,它就会对实例中的变量进行默认初始化。
一旦虚拟机完成了为新对象分配内存和为实例中的变量默认初始化之后,接着就是为实例中的变量赋真正的初始值。
1、如果对象通过clone调用来创建的,虚拟机把被克隆的实例变量中的值拷贝在新的对象中。
2、如果对象是通过ObjectInputStream的readObject方法调用反序列化的,虚拟机通过从输入流中读入的值来初始化实例中的变量。
3、虚拟机调用对象的实例初始化方法来进行变量的初始化。
上面的整个过程的流程图如下;
Java编译器会为每个类至少生成一个实例初始化化方法,在Java的class文件中,它就是“<init>
”方法,对应的就是Java文件中的构造方法,针对每一个类的构造方法,Java编译器都产生一个<init>
方法。如果类中没有声明任何构造方法,编译器默认会产生一个无参构造方法。
一个<init>()
方法中可能包含三种代码:
1、调用另一个<init>
方法
2、实现对实例中的变量的初始化
3、构造方法体代码
如果构造函数没有明确地调用this()或者super(),并且设个对象不是Object对象,对应的<init>
方法默认会调用超类的无参<init>
方法。如果构造方法通过明确地调用超类的构造方法(super()),它的<init>
方法就会调用对于超类的<init>
方法。这个构造函数对于的<init>
方法由以下几个部分组成:
1、一个超类的<init>
方法调用
2、实例中变量初始化代码
3、这个构造函数方法体代码
下面我们来举个例子:
public class Base {
public static void main(String[] args) {
Sub b = new Sub();
}
}
class MyParent {
private String parentName = "base";
public MyParent(){
System.out.println (parentName);
}
}
class Sub extends MyParent {
private String baseName = "sub";
public Sub(){
System.out.println (baseName);
}
}
上面Sub类实例生成的过程如下:
最终的输出结果就一目了然了:
base
sub
如果构造方法没有使用this()开始调用,并且这个类就是Object,因为Object没有超类,所以上面的<init>
就不会调用其超类的<init>
方法了。
如果构造方法通过明确的调用同一个类中的另一个构造方法,它对于的<init>
方法分为一下两个部分:
1、另一个构造函数对于的<init>
方法调用。
2、构造函数方法体代码
下面我们举个例子:
public class Base {
public static void main(String[] args) {
MyParent b = new MyParent();
}
}
class MyParent {
private String parentName = "base";
public MyParent(){
this("hello");
System.out.println ("MyParent()");
}
public MyParent(String name) {
System.out.println ("MyParent(String name):" + name);
}
}
整个执行的流程图如下:
从上面的过程也可以很容易的看出执行的结果:
MyParent(String name):hello
MyParent()
了解了上面的流程,下面我们来看看一个笔试题。
携程Java工程师——一道面向对象面试选择题
public class Base
{
private String baseName = "base";
public Base()
{
callName();
}
public void callName()
{
System. out. println(baseName);
}
static class Sub extends Base
{
private String baseName = "sub";
public void callName()
{
System. out. println (baseName) ;
}
}
public static void main(String[] args)
{
Base b = new Sub();
}
}
上面的代码输出结果是什么?
下面我们来看看执行流程图:
从上面我们可以看出callName()函数的调用在Sub的baseName变量初始化之前,在默认初始化的时候baseName会被初始化为null,当调用callName的时候,它会调用子类的callName方法,这个时候子类Sub的变量baseName还没有被初始化,所以最终的输出结果为null。
垃圾收集和对象终结
程序可以明确或者隐含的为对象分配内存,但是不能明确地是否内存,但是当一个对象不再为程序所引用了,虚拟机必须回收那部分内存,在虚拟机中定义了一些垃圾回收算法来定时的对垃圾进行回收。垃圾回收器单独运行在一个线程中。
如果类声明了一个名为finalize()的方法,垃圾收集器会在释放这个实例所占据的内存空间之前执行这个方法一次。垃圾回收器最多只会调用一个对象的终结方法一次。
卸载类型
在Java虚拟机中类的生命周期和对象的生命周期很相似。虚拟机创建并初始化对象,使程序使用对象,然后在对象变得不再被引用后可选地进行垃圾收集。同样,虚拟机装载、连接并且初始化类,使程序能使用类,当程序不再引用他们的时候可选的卸载它们。
如果程序不再引用某类型,那么这个类型就变成不可触及,所以可以被垃圾收集。
使用启动类加载器装载的类型永远是可触及的,所以永远不会被卸载。只有使用用户定义的类装载器装载的类型才会变成不可触及的,从而被虚拟机回收。
判断动态装载类型的Class实例在正常的垃圾收集过程中是否可以触及有两种方式:
1、如果程序保持对Class实例的明确引用,它就是可触及的。
2、如果在堆中存在一个可触及的对象,在方法区中它的类型数据执行一个Class实例,那么这个Class实例就是可触及的。