浅谈JVM的实现与垃圾回收机制
Java被称为是一个人类可读的编程语言,其主要特点是基于类和面向对象,Java的开源版本被称为OpenJDK。Java编程环境由两个部分组成:Java语言和运行环境,运行环境也称为Java虚拟机(JVM),JVM是一个为执行Java程序提供运行时环境的程序。本文主要探讨JVM的实现机制。
什么是JVM
解释之前,先上一张图吓一下大家:
这张图中我们需要注意的是,JVM的核心组件包括三个部分:Heap, JIT Compiler, GC, 当我们要优化JVM的性能的时候主要调优的目标是这三个组件。
JVM负责解释执行字节码文件,所有平台上的JVM向编译器提供相同的编程接口,而编译器只需要面向虚拟机,生成虚拟机能理解的代码,然后由虚拟机来解释执行。JVM是一个为执行Java程序提供运行时环境的程序。没有JVM,Java程序也就不能执行。一个Java程序通常通过以下的方式执行:
java <arguments> <program name>
首先操作系统会启动JVM进程为Java程序提供运行时环境,然后会在刚启动的虚拟机中执行我们的Java程序。需要注意的是,Java程序首先要被转换(编译)成Java字节码(bytecode),JVM上运行的是Java字节码程序。JVM可以看成是一个JAVA字节码程序解释器。
Write Once, Run anywhere
当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码不面向任何具体平台,它只面向JVM。不同平台的JVM都是不同的,但它们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行。
JVM收集运行时信息以为如何执行代码做出更好的决策。这意味着JVM能够自动模拟和优化运行在它上面的程序。JVM在Java程序和操作系统间形成了一个中间层,使得开发者不用过多的处理和操作系统相关的具体细节问题。
JVM的另一个好处是,任何能够编译成字节码的语言都可以在它上面运行,不仅仅是Java,比如Groovy, Scala和Clojure这些基于JVM的语言。这也意味着,这些语言可以轻松的使用其他语言写的函数库。例如一个Scala开发者可以调用Java库,因为他们运行在相同的平台上。
从实际硬件上隔离出来,意味着Java代码就像一个沙盒,这可以避免一运行有害代码对机器硬件造成的伤害。安全性是JVM的主要好处之一。需要注意的是,并不是所有的JVM都是相同的,在Oracle的标准JVM实现标准之上还存在很多其他的实现方式。而我们主要讨论的是HotSport JVM,这也是OpenJDK和Oracle JVM的实现基础。
JIT - Just In Time
正如我们之前讨论的,JVM运行的是字节码。但是,如果有一段代码会被频繁的执行,那么JVM可以决定将这段代码编译为机器码(native code)以加快这段代码的执行速度。JIT可编译的最小执行块是一个方法。默认情况下,一个代码块需要被执行1500次才会被JIT编译,这个次数是可配置的。利用JIT机制可以有效的提升系统的性能,但是,JIT编译并不是无代价的,其需要耗费额外的系统资源和时间。
Java内存管理和GC
在Java中,对象所占用的内存在对象不再使用后会自动被回收。这些工作是由一个叫垃圾回收器Garbage Collector
的进程完成的。相比较C/C++程序而言,你必须手动调用free()
函数或delete
操作符来回收内存。在这里我们主要讨论的是HotSpot JVM(这也是OpenJDK和Oracle Java的实现基础),事实上,还有很多其他的JVM实现方式。
GC的优缺点
好处是
- 开发者无需过问内存管理,可以专注于解决实际问题。虽然内存泄露仍有可能会发生,但发生的概率比较小。
- GC的智能算法可以在后台自动的进行内存管理,且这种管理在大多数时候是最佳的。
坏处是
- 当垃圾回收发生时,它会影响程序的性能,甚至会暂停程序的执行。这个被称为“Stop the world”垃圾回收机制,整个程序进程会被暂停以等待垃圾回收执行完。对某些应用而言,这可能是无法接受的。
- 开发者并不能指定何时或使用何种方法执行GC。
Generational GC
在了解更多关于GC的问题之前有必要理解Java内存中的堆区(Heap)事如何工作的。所有的对象都存在于堆中(与此对应的是栈,这里初访者变量和方法,以及堆中对象的引用)。垃圾回收的过程也就是将堆区中不再需要对象清除的过程。几乎所有的GCs都是“分代的(generational)”,也就是说堆区会划分成很多的区块,也称为代。Hotspot的堆结构如下图所示:
New/Young Generation
大多数的应用中持有的对象很大部分是短生命周期的,这被称为“Weak generational hypothesis”。在垃圾回收期间分析应用中所有的对象是一件缓慢而耗时的工作,因此可以将短生命周期的对象在其被创建时就分隔出来。因此New Generation
可以进一步划分为:
- Eden Space (Eden空间):所有的新创建的对象都存在与此。当其变满时,
minor GC
便会出现。然后所有仍然被引用的对象被移动到幸存者空间中。 - Survivor Spaces (幸存者空间):对不同的JVM而言,幸存者空间的实现方式也不尽相同,但基本原理都是相同的。New Generation中的每一个GC都会增加幸存者空间中的对象年龄。如果对象的年龄超过某个特定值(默认情况下是15),该对象会被移往Old Generation。
New Generation中的GC也被称为minor GC
。使用New Generation
的好处是可以减少分片带来的影响。
Old Generation
任何从New Generation中的幸存者空间中幸存下来的对象会被送往Old Generation。Old Generation通常比New Generation大很多。存在于Old Generation中的GC也被称为Full GC
。Full GC可以执行“Stop The World”机制,并且通常会占用更长的时间,因此Full GC也称为绝大不多的JVM可以进行优化的地方。
Permanent Generation
Permanent Generation用于存放类的元信息。在Java 8中其被metaspace所取代。通常Permanent Generation无需为了确保其有足够空间而被优化,但是当类没有被正确上传时,其仍有可能发生内存泄露的情况。
Java中的内存泄露
Java所支持的垃圾回收机制有效的减少了内存溢出的发生。内存溢出意味着当分配出去的内存却永远都没有被回收。虽然JVM能够自动回收那些没有被使用的对象所占用的内存,但事实上,Java中还是回发生内存溢出现象。假设存在一个有效的(但是没有被使用)对象引用指向一个没有被使用的对象,这会导致该对象一直占用着内存。
举例来说,当一个方法需要运行很长的时间(或者永远都在运行),方法中的局部变量可以在长时间的持有对象引用,而这远超出自己实际需要的时间。代码如下:
public static void main(String args[]) {
int bigArray[] = new int[1000];
int result = compute(bigArray);
// We no longer need bigArray. It will get garbage collected when
// there are no more references to it. Because bigArray is a local
// variable, it refers to the array until this method returns. But
// this method doesn't return. So we've got to explicitly get rid
// of the referenceourselves, so the garbage collector knows it can
// reclaim the array.
bigArray = null;
// Loop forever, handling the user's input
for(;;) handle_input(result);
}
内存泄露也会出现在你使用类似于HashMap这类数据结构时将一个对象与另一个对象相关联的情况。即使两个对象都不再被需要了,这种关联仍会存在于hash表中,除非hash表本身被垃圾回收器回收了,否则其中的关联对象会一直占用内存。如果hash表会运行相当长的时间的话,那么内存泄露便会发生。
System.gc() and finalize()
可以手动执行垃圾回收嘛?
这应该是个有意思的问题。答案是可以,也不可以。我们可以调用System.gc()
方法建议JVM执行垃圾回收。然后,并没有任何保证说JVM一定会执行该操作。作为开发者,我们无法知晓JVM是否执行了我们的代码。并且,通常认为使用System.gc()
是个很不明智的做法。
finalize()
finalize()
方法存在于java.lang.Object
类中,可以被所有对象所使用。默认情况下其不执行任何动作。当垃圾回收器确定了一个对象没有任何引用时,其会调用finalize()
方法。但是,finalize方法并不一定会被执行,因此也不建议覆写finalize()
该方法。