JVM 根据对象的存活周期不同,把堆内存划分为三块,一般为新生代、老年代、永久代(对于HotSpot虚拟机而言),在绝大多数情况下,新创建的对象首先分配在eden区,在一次新生代回收之后,如果对象还存活,则进入s0或者s1,每经过一次新生代回收,对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。常量、类变量、类信息等会存放在永久代中。方法区是JVM规范的一部分,是规范,而永久代是方法区的一种实现。
对堆内存分代是为了提高内存空间利用率及垃圾回收器的效率。
通过内存分代只需要在新生代中经行频繁的GC即可,而老年代中对象生命周期长,进行GC频率相对较低,永久代一般都不进行垃圾回收。JVM还可以根据不同年代的特点采用合适的垃圾回收算法,这样可以大大的提高垃圾回收效率。
新生成的对象优先存放在新生代中(大对象除外,大对象会之间进入老年代),新生代中的对象存活率很低,一般进行一次垃圾回收可回收 80% 的空间。
HotSpot(一种JVM的实现)中,将新生代划分为三块,一块较大的eden区还有两块较小的Survivor 空间也就是s1区和s2区,默认比例为8 : 1 : 1。这样划分的目的是为了提高内存空间的使用率(个人理解是因为一次gc就回收掉了大部分对象,只有小部分才能进入Survivor 空间)
当 eden区 没有足够空间分配个新生对象时,虚拟机将发起一次 Minor GC,s1区 是作为保留区的,GC 之前是空的,进行 GC 的时候会将 eden区 中所有的存活对象放入 s1 中。而 s0 存活的对象会根据年龄决定去向,如果年龄达到阈值(经历一次GC年龄+1,阈值默认是15)会进入老年代,没有达到阈值的也会被复制到s1中, 随后会清空 eden区 和 s0区,如果GC时s1中没有足够空间会放一部分对象放入老年代。GC 结束后 s1 跟 s0 的角色会互换.
在新生代中经历了多次GC后仍然存活的对象会进入老年代中。能进入老年代的对象都是生命周期较长,存活率高的对象,所以在老年代中经行GC的频率相对新生代而言要低。
永久代存储类信息、常量、类变量、即时编译器编译后的代码等数据,对于这一区域而言,Java虚拟机规范中值出可以不用对其进行垃圾回收,一般不会对其经行垃圾回收。
注意:永久代在JDK1.8中已经移除
对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。
一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。
public class A {
…
}
这个声明等同于下面的声明:
public class A extends java.lang.Object {
…
}
避免在循环体中创建对象,即使该对象占用内存空间不大。
for (int i = 0; i < 10000; ++i) {
Object obj = new Object();
System.out.println("obj= "+ obj);
}
**上面这种写法违法了该规则,会浪费大量空间。**
Object obj = null;
for (int i = 0; i < 10000; ++i) {
obj = new Object();
System.out.println("obj= "+ obj);
}
这种写法,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。
强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。
软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。
import java.lang.ref.SoftReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,将它设置为soft 引用类型,并且释放强引用;
SoftReference sr = new SoftReference(a);
a = null;
…
// 下次使用时
if (sr!=null) {
a = sr.get();
}
else{
// GC由于内存资源不足,可能系统已回收了a的软引用,
// 因此需要重新装载。
a = new A();
sr=new SoftReference(a);
}
GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。
import java.lang.ref.WeakReference;
…
A a = new A();
…
// 使用 a
…
// 使用完了a,将它设置为weak 引用类型,并且释放强引用;
WeakReference wr = new WeakReference (a);
a = null;
…
// 下次使用时
if (wr!=null) {
a = wr.get();
}
else{
a = new A();
wr = new WeakReference (a);
}
虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。
public void process () {
try {
Object obj = new Object();
obj.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
while (isLoop) { // ... loops forever
// 这个区域对于obj对象来说已经是不可视的了
// 因此下面的代码在编译时会引发错误
obj.doSomething();
}
}
如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。
在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。
当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。
JVM 这里默认使用HotSpot虚拟机。简单回顾一下JVM内存结构,JVM中主要将使用到的内存划分为五块,其中:
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就相当于对这个类创建一个具体实现。所以我们可以了解到,一个对象他首先必须可以指明所属的类,其次它还必须能指明他所特指的哪一个具体实现。
对应的有两种实现方式:
HotSpot采用的是第二种实现方式。
Class的装载包括3个步骤:加载(loading),连接(link),初始化(initialize)
根据上图所示,我们不难理解,当一个对象进行实例化的时候,JVM会根据所需对象类型在JAVA堆中划分内存区,并生成指向方法区对象数据类型的指针用以标识对象。
虚拟机栈中的本地变量表(也有称为局部变量表)中指针指向JAVA堆中划分好的内存区域。JAVA虚拟机采用动态链接方式,只有编译后的class文件并未存储最终方法在内存的表现形式。
初始化实际上是对class文件中的初始化方法进行调用,其核心还是虚拟机栈中栈帧的一次POP/PUSH。相当于对类中的对象进行一次同样的装载过程。
至此,一个对象完整的实例化过程就全部介绍完毕。
这块之所以单独摘出,是因为这是经常用到却又容易被忽略的一个地方。
根据 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!
很明显,对象类的静态域和静态方法都被执行了。
JAVA与传统语言的最大区别莫过于对于内存的控制。在传统语言当中,我们都是直接控制内存,通过malloc()这种方法申请内存,但无可避免的,在内存使用过程中还涉及到内存的释放,在C中我们熟悉的方式是通过free()进行释放。JAVA程序是运行于JAVA 虚拟机(JVM,Java Virtual Mechine)之上,在初期接触JAVA时,我们似乎没有发现我们需要对内存进行控制,那是因为JVM已经帮助我们完成了JAVA对象的内存的申请与回收。