学习目标:掌握对象的生命周期、内存分配与垃圾收集器。
对象的生命周期
一个对象的生命周期由实例化阶段、使用阶段、释放阶段构成。
实例化阶段
有四种方式可以实现对象的实例化:第一种方式是使用new运算符进行对象的实例化;第二种方式是使用反射技术的newInstance()方法实例化对象;第三种方式是通过ObjectInputStream类的readObject()方法反序列化;第四种方式是通过对象的clone()方法进行现有对象的克隆。
案例1:建立Person类,定义name和age两个成员变量,定义showPerson()方法,输出name和age的值。建立PersonTest测试类,分别使用new运算符、反射技术的newInstance()方法、对象的clone()方法实例化对象。
新建项目PUnit14,在PUnit14项目新建memory包,在memory包下新建Person类。代码如下:
package memory;
public class Person {
// name
String name;
// age
int age;
public Person(String name,int age)
{
this.name = name;
this.age = age;
}
public void showPerson()
{
System.out.println("姓名:" + this.name);
System.out.println("年龄:" + this.age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
Object obj = super.clone();
Person person = (Person)obj;
person.setName(this.name);
person.setAge(this.age);
return obj;
}
}
Person类需要实现Cloneable接口的clone()方法,进行对象内容的复制。
在memory包下新建PersonTest类。代码如下:
package memory;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class PersonTest {
public static void main(String[] args) {
// 使用new运算符实例化Person对象
Person persona = new Person("张三",21);
persona.showPerson();
System.out.printf("persona对象的哈希码:%s\n",persona.hashCode());
// 使用反射技术的newInstance()方法实例化Person对象
Class<?> personClass = (Class) Person.class;
try {
Constructor paramConstructor = personClass.getConstructor(String.class, int.class);
Person personb = (Person) paramConstructor.newInstance("李四", 36);
if( null != personb )
{
personb.showPerson();
System.out.printf("personb对象的哈希码:%s\n",personb.hashCode());
}
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InstantiationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 使用对象的clone()方法复制对象
try {
Person personc = (Person)persona.clone();
personc.showPerson();
System.out.printf("personc对象的哈希码:%s\n",personc.hashCode());
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
PersonTest程序分别使用new运算符、反射技术的newInstance()方法、对象的clone()方法实例化对象。
程序执行结果如下图所示:

当程序使用上面的方法实例化对象时,虚拟机会在堆中为对象分配内存,所有在对象的类中和父类中声明的变量都要分配内存,对象内存分配完成后,虚拟机会将实例变量初始化为默认的初始值,随后为实例变量赋正确的初始值。
使用阶段
对象被实例化后,虚拟机会把对象在堆中的内存首地址返回给内存申请者。此时对象被类变量或方法内的局部变量引用,对象进入使用阶段。虚拟机的垃圾收集器不会回收被有效变量引用的对象,这里的有效变量是指变量在有效范围内。例如在方法的执行过程总,方法的局部变量是有效的,方法执行完成后,方法内的局部变量就是无效的了。
释放阶段
当一个实例对象不再为程序所用时,也就是说当对象不再被有效的变量引用时,此时对象进入释放阶段。虚拟机必须释放这些实例对象占有的存储空间,此时实例对象被终结。当一个实例对象不再使用时,虚拟机并不是立即释放这些实例对象,而是要等待垃圾收集器再次进入回收周期后来释放这些实例对象,回收堆中的存储空间。
String对象的内存分配
String对象有三种创建方式:
第一种方式是直接通过赋值语句,将字符串赋值给String类型的变量。
例如:
String str = “Hello”;
第二种方式是通过new运算符,实例化一个String对象,并将对象引用赋值给String类型的变量。
例如:
String str = new String(“Hello”);
第三种方式是通过String对象的intern()方法返回一个String对象的引用。
例如:
String str = “Hello”. intern();
前面String对象的三种创建方式,虚拟机对其内存分配上是有所区别的,先来看第一种创建方式。
第一种创建方式是通过赋值语句直接将字符串赋值给String类型的变量。在这种创建方式中,虚拟机会在方法区的常量池中判断是否存在具有和字符串(如Hello)内容相同的String对象:如果常量池不存在和赋值字符串内容相同的对象,虚拟机就在常量池中分配内存并创建该String对象,并将String对象的引用赋值给String类型的变量;如果常量池存在与赋值字符串内容相同的对象,虚拟机会直接将该对象的引用赋值给String类型的变量。这种创建方式对连续创建同一字符串内容的String对象特别有用,内存利用效率非常高效。
第二种创建方式是通过new运算符实例化String对象,并将new运算符返回的对象引用赋值给String类型的变量。在这种创建方式中,虚拟机会创建两个String对象:一个String对象是在常量池中创建,如果常量池中已有字符串内容相同的对象,则不创建;一个String对象是在运行数据区的堆中创建,将在常量池中创建的String对象的字符数组复制到在堆中创建的String对象。
String类型的变量接收new运算符返回的对象引用后,如果使用赋值语句对该String类型的变量重新赋予不同的字符串内容,该变量将会指向一个新的String对象,该String对象会在常量池中创建。
案例2:建立StringTest1类,在类的main()方法内部,使用new运算符实例化一个String对象,返回的对象引用赋值给String类型的变量str,输出str指向对象的哈希码,然后使用赋值语句将新的字符串内容赋值给str,输出str指向对象的哈希码,验证哈希码是否一致。
在memory包下新建StringTest1类。代码如下:
package memory;
public class StringTest1 {
public static void main(String[] args) {
// 实例化String对象
String str = new String("Hello");
System.out.println("str对象的哈希码:" + str.hashCode());
// 修改str对象的内容
str = "Hello World";
System.out.println("str对象修改后的哈希码:" + str.hashCode());
}
}
程序执行结果如下图所示:

从程序的执行结果可以看出,当对str重新赋值不同的内容后,虚拟机会在常量池创建一个新的String对象,并将该对象的引用赋值给str。
第三种创建方式是通过String对象的intern()方法来返回一个String对象的引用。在这种创建方式中,虚拟机会首先判断在常量池中是否存在“Hello”字符串对象,如果存在就直接返回该对象的引用,否则就在常量池创建该对象,并返回对象的引用。
前面String对象的内存分配经常用到常量池,常量池是虚拟机从运行数据区的方法区划分出来的一块内存区域,JDK1.8将常量池放置到运行数据区的堆区域。常量池主要用来存储字面常量、使用final修饰的变量以及符号引用。字面常量包括数值常量(如36、100等)、字符串常量(如“123”、“abc”等)。符号引用是指用一组符号来描述引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如编译器会把对象的引用作为一个符号引用,因为编译器不知道对象引用在内存的实际地址,当虚拟机加载类到运行数据区并初始化类后,虚拟机会把这些符号引用转换为直接引用(指向目标的内存地址,如对象在堆中的内存地址)。
垃圾收集器
垃圾收集器(Garbage Collection)通常被称为GC,它用于回收在堆中不再使用的实例对象和数组占用的内存空间。除了释放不再被引用的对象外,它还负责对堆中内存进行碎片整理,将多个碎片内存区域整理成一块较大的内存区域,提供堆中内存的利用效率。
GC需要一种算法来判断堆中的实例对象是否可以被释放:当实例对象不再被有效变量引用时,这些实例对象是可以被释放的;当实例对象正在被有效变量引用时,这些实例对象是不能被释放的。
引用计数是GC使用比较早的一种算法。在这种算法中,每个实例对象都有一个引用计数,当对象在堆中创建后,并且对象的引用被赋值给一个变量,这个对象的引用计数就加1。当引用的变量被释放或被赋予一个新的对象引用后,该对象的引用计数就减1,对象的引用计数变为0后,GC就会认为这个对象是可以被释放的。引用计数算法虽然简单,但会存在循环引用的问题,即两个或者更多的对象相互引用。例如:父对象有一个对子对象的引用,子对象又反过来引用父对象,就会导致这些对象的计数不可能变为0。因此引用计数的算法在虚拟机中已经很少使用了。
目前主流的虚拟机大多采用分区收集算法。它根据对象的存活周期的不同而将内存分为几块,分别为新生区、老年区和静态区。新生区的对象生存期短,每次回收都会有大量对象被释放(例如方法的局部变量等);老年区里的对象存活率较高(例如类变量等);静态区里的对象创建后几乎不用释放(如Class对象、静态变量等)。
GC的触发机制
GC有两种类型:Full GC 和Scavenge GC。
1、Full GC
GC在优先级最低的线程中运行,一般在线程空闲时(即没有线程在运行)被调用。GC会对整个堆进行整理,包括新生区、老年区和静态区。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。
2、Scavenge GC
(1)当堆内存不足时,会触发Scavenge GC调用,释放堆中不再被变量引用的实例对象或数组;
(2)线程在运行过程中创建新对象,若这时堆空间不足,JVM就会强制调用Scavenge GC线程。若GC一次之后仍不能满足内存分配,JVM会再进行两次GC,若仍无法满足要求,则JVM将报“out of memory”的错误,虚拟机将停止运行,程序也会停止运行。
GC的两个重要方法
(1)System.gc()方法
程序调用System.gc()方法,可以触发GC进行一次垃圾回收。
(2)finalize()方法
该方法用于在GC释放一个对象之前,会调用finalize()方法释放对象用到的资源。如果程序要释放对象用到的资源,需要重写父类的finalize()方法,Object类定义了finalize()方法。