正文
快要讲解 Java 垃圾回收机制了,在此之前我们有必要了解一下 Java 对象的内存分配和创建过程。
JDK8内存区域
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。如下图所示:
Java 对象分配内存主要与堆有关,所以此处只介绍一下堆内存。
堆是 JVM 内存管理的最大的一块区域,此内存区域的唯一目的就是存放对象的实例,所有对象实例与数组都要在堆上分配内存。它也是垃圾收集器的主要管理区域。java 堆可以处于物理上不连续的空间,只要逻辑上是连续的即可。线程共享的区域。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出 OutOfMemoryError 异常。
为了支持垃圾收集,堆被分为三个部分:
- 年轻代 : 常常又被划分为Eden区和Survivor(From Survivor To Survivor)区(Eden空间、From Survivor空间、To Survivor空间(空间分配比例是8:1:1)
- 老年代
- 永久代 (jdk 8已移除永久代)
(1)、堆是 JVM 中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了 new 对象的开销是比较大的。
(2)、Sun Hotspot JVM 为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间 TLAB(Thread Local Allocation Buffer),其大小由 JVM 根据运行的情况计算而得,在 TLAB 上分配对象时不需要加锁,因此 JVM 在给线程的对象分配内存时会尽量的在 TLAB 上分配,在这种情况下 JVM 中分配对象内存的性能和 C 基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
(3)、TLAB 仅作用于新生代的 Eden Space,因此在编写 Java 程序时,通常多个小的对象比大的对象分配起来更加高效。
(4)、所有新创建的 Object 都将会存储在新生代 Yong Generation 中。如果 Young Generation 的数据在一次或多次 GC 后存活下来,那么将被转移到 OldGeneration。新的 Object 总是创建在 Eden Space。
值得注意的是,我们说 TLAB 是线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。
换言之,虽然每个线程在初始化时都会去堆内存中申请一块 TLAB,并不是说这个 TLAB 区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。
Java对象创建方式
在 Java 程序中,我们拥有多种新建对象的方式。
最简单的方式就是使用new关键字。
User user = new User();
除此以外,还可以使用反射机制创建对象:
User user = User.class.newInstance();
或者使用 Constructor 类的 newInstance:
Constructor<User> constructor = User.class.getConstructor();
User user = constructor.newInstance();
除此之外还可以使用 clone 方法、反序列化以及 Unsafe 类的方式,Object.clone 方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。Unsafe.allocateInstance 方法则没有初始化实例字段,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。想深入这三种方式的朋友,推荐阅读 java创建对象的五种方式
以 new 语句为例,它编译而成的字节码将包含用来请求内存的 new 指令,以及用来调用构造器的 invokespecial 指令。
public class SubUser {
public static void main(String[] args) {
SubUser subUser = new SubUser();
}
}
// 解析字节码文件
public com.msdn.java.hotspot.object.SubUser();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/msdn/java/hotspot/object/SubUser
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
提到构造器,就不得不提到 Java 对构造器的诸多约束。首先,如果一个类没有定义任何构造器的话, Java 编译器会自动添加一个无参数的构造器。
Object 类是一切 Java 类的父类,对于普通的 Java 类,即便不声明,也是默认继承了Object 类。正如上面案例所示,SubUser 没有显式声明构造方法,Java 编译器会自动添加对父类构造器的调用。但是,如果父类没有无参数构造器,那么子类的构造器则需要显式地调用父类带参数的构造器。
显式调用又可分为两种,一是直接使用“super”关键字调用父类构造器,二是使用“this”关键字调用同一个类中的其他构造器,具体可查看下述示例代码。无论是直接的显式调用,还是间接的显式调用,都需要作为构造器的第一条语句,以便优先初始化继承而来的父类字段。(不过这可以通过字节码注入来绕开,我们之前讲重载时有介绍到)
public class Cutomer {
private String name;
private int age;
public Cutomer(String name, int age) {
this.name = name;
this.age = age;
}
}
public class SubUser extends Cutomer {
private long id;
public SubUser(String name, int age) {
super(name, age);
}
public SubUser(String name, int age, long id) {
// super(name, age);
this(name, age);
this.id = id;
}
}
总而言之,当我们调用一个构造器时