JAVA对象实例化过程
Class初始化理解
此篇中详细介绍了JAVA对象的实例化过程
JAVA对象内存分配过程
JVM 这里默认使用HotSpot虚拟机。简单回顾一下JVM内存结构,JVM中主要将使用到的内存划分为五块,其中:
- 线程私有:虚拟机栈(VM Stack),本地方法栈(Native Method Stack),程序计数器(Program Counter Register)
- 线程共享:JAVA堆(Heap),方法区(Method Area)
JAVA对象实例化过程中,主要使用到的包括虚拟机栈,JAVA堆和方法区。
JAVA文件经编译之后首先会被加到到JVM方法区,JVM方法区中很重要的一个部分是运行时常量池——用以存储class文件类的版本、字段、方法、接口等描述信息和编译期间的常量和静态变量。
JAVA对象真正进行实例化的地方在JAVA堆和虚拟机栈中,这里无可避免的需要引入指针的概念。
In computer science, a pointer is a programming language object, whose value refers to (or “points to”) another value stored elsewhere in the computer memory using its address. A pointer references a location in memory, and obtaining the value stored at that location is known as dereferencing the pointer. As an analogy, a page number in a book’s index could be considered a pointer to the corresponding page; dereferencing such a pointer would be done by flipping to the page with the given page number. -Wiki
Object A = New Object();
在实际内存中,A其实相当于我们给Ojbect这个类的实现起的一个名字,在面向对象编程中,就像狗是属于一类动物,但是特指的那一条狗我们会给他起一个名字用以区分一样。Object用以标记A是属于这个类,而A是特指Object的一个具体实现,而New Object就相当于对这个类创建一个具体实现。所以我们可以了解到,一个对象他首先必须可以指明所属的类,其次它还必须能指明他所特指的哪一个具体实现。
对应的有两种实现方式:
1.句柄访问对象
2.直接指针访问对象
HotSpot采用的是第二种实现方式。
Class的装载包括3个步骤:加载(loading),连接(link),初始化(initialize)
加载
根据上图所示,我们不难理解,当一个对象进行实例化的时候,JVM会根据所需对象类型在JAVA堆中划分内存区,并生成指向方法区对象数据类型的指针用以标识对象。
链接
虚拟机栈中的本地变量表(也有称为局部变量表)中指针指向JAVA堆中划分好的内存区域。JAVA虚拟机采用动态链接方式,只有编译后的class文件并未存储最终方法在内存的表现形式。
初始化
初始化实际上是对class文件中的初始化方法进行调用,其核心还是虚拟机栈中栈帧的一次POP/PUSH。相当于对类中的对象进行一次同样的装载过程。
至此,一个对象完整的实例化过程就全部介绍完毕。
Static 静态化
这块之所以单独摘出,是因为这是经常用到却又容易被忽略的一个地方。
根据 Class初始化理解的实验,我们可以看出实例化并非在程序运行后就立马将所有的静态变量都进行装载。
首先做这样的一个试验
public class StaticObject {
//创建静态域,Intalized类构建方法仅用于输出标识信息
public static Initalized initalized = new Initalized("Static object is loaded");
//创建静态方法
public static void loadTheClass() {
System.out.println("Static bject has benn loaded!");
}
}
public class StaticImplement {
public static void main(String[] args) {
//测试StaticObject类是否执行静态域,静态方法
StaticObject impl = null;
}
}
执行之后,明显没有输出结果。我们知道,在Import或者StaticObject impl = null;
这样的声明之后,类实际已经被加载。这说明,类在被加载之后,并不会初始化方法域。
//修改StaticImplement类如下
public class StaticImplement {
public static void main(String[] args) {
//测试StaticObject类是否执行静态域,静态方法
StaticObject impl = null;
//执行静态方法
impl.loadTheClass();
}
}
执行结果如下
Static object is loaded
Static bject has benn loaded!
很明显,对象类的静态域和静态方法都被执行了。
JVM回收机制
JAVA与传统语言的最大区别莫过于对于内存的控制。在传统语言当中,我们都是直接控制内存,通过malloc()
这种方法申请内存,但无可避免的,在内存使用过程中还涉及到内存的释放,在C中我们熟悉的方式是通过free()
进行释放。JAVA程序是运行于JAVA 虚拟机(JVM,Java Virtual Mechine)之上,在初期接触JAVA时,我们似乎没有发现我们需要对内存进行控制,那是因为JVM已经帮助我们完成了JAVA对象的内存的申请与回收。
JAVA对象状态
阅读此节请先阅读上篇,了解对象存在的8种状态。
对象销毁详解
JAVA对象在其生命周期中存在两个属性,可达和终结。
可达分为:可达,终接器-可达,不可达
终结分为:未终结,可终结,已终结
笛卡尔积后,不存在 终接器-可达 且 已终结状态,故共有八种状态。
可达方面,在JVM不断改进过程中,不可达状态被JVM严格防止,因为不可达状态会导致内存泄漏(Memory Leak)。当前使用的JDK一般无法对对象进行内存级的操作,但可以通过Unsafe进行直接的内存操作,详细可以参照:
如何对JAVA进行内存操作 sun.misc.Unsafe类
以下示例展示对象GC过程中能遇到的八种状态
/**
* 对象GC状态测设
* @author CunChen
*
*/
public class FinalizeEscapeGCTest {
public static FinalizeEscapeGCTest test = null;
public void isAlive() {
System.out.println("alive!");
}
//在第一次调用的时候被静态变量引用自救
@Override
protected void finalize() throws Throwable {
// Finalizer-Reachable Finlizable
super.finalize();
System.out.println("finalize method executed!");
// Reachable Finlizable
FinalizeEscapeGCTest.test = this;
}
public static void main(String[] args) throws InterruptedException {
// Reachable Unfinalized
test = new FinalizeEscapeGCTest();
System.out.println("start");
// Finalizer-Reachable Unfinlized
test = null;
//第一次成功自救
System.gc();
//finalize方法优先级低,等待0.5秒
Thread.sleep(500);
if (test != null) {
test.isAlive();
} else {
System.out.println("Died!");
}
//第二次自救失败
test = null;
// Reachable Finalized
System.gc();
//finalize方法优先级低,等待0.5秒
Thread.sleep(500);
// Unreachable Finalized
if (test != null) {
test.isAlive();
} else {
System.out.println("Died!");
}
}
}