(obj, null); obj = null;
垃圾收集算法 1. 标记 - 清除
在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。 在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。 在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于
size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为
size 的分块,并把大小为 (block - size) 的块返回给空闲链表。 不足:
标记和清除过程效率都不高; 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2. 标记 - 整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 优点:
不足:
需要移动大量对象,处理效率比较低。
3. 复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 主要不足是只使用了内存的一半。 现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor
空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块
Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。 HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于
10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
4. 分代收集现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 一般将堆分为新生代和老年代。
新生代使用:复制算法 老年代使用:标记 - 清除 或者 标记 - 整理 算法
垃圾收集器
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程; 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
1. Serial 收集器
Serial 翻译为串行,也就是说它以串行的方式执行。 它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
2. ParNew 收集器
它是 Serial 收集器的多线程版本。 它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
3. Parallel Scavenge 收集器与 ParNew 一样是多线程收集器。 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。 缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和
Survivor
区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
4. Serial Old 收集器
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5. Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6. CMS 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 分为以下四个流程:
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 具有以下缺点:
吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 无法处理浮动垃圾,可能出现 Concurrent Mode
Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC
时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS
收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode
Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
7. G1 收集器 G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region
的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个
Region
垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的
Region。 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
初始标记 并发标记 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的
Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered
Set 中。这阶段需要停顿线程,但是可并行执行。 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
包括以下 7 个阶段:
其中二进制字节流可以从以下方式中获取:
从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。 从网络中获取,最典型的应用是 Applet。 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
2. 验证 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3. 准备 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。 public static int value = 123; 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。 public static final int value = 123;
4. 解析 将常量池的符号引用替换为直接引用的过程。 其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
5. 初始化 初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码: public class Test { static { i = 0; // 给变量赋值可以正常编译通过 System.out.print(i); // 这句编译器会提示“非法向前引用” } static int i = 1; } 由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码: static class Parent { public static int A = 1; static { A = 2; } }
static class Sub extends Parent { public static int B = A; }
public static void main(String[] args) { System.out.println(Sub.B); // 2 } 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 ()
方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 ()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 ()
方法。 虚拟机会保证一个类的 ()
方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 ()
方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 ()
方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
类初始化时机
1. 主动引用虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
遇到 new、getstatic、putstatic、invokestatic
这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new
关键字实例化对象的时候;读取或设置一个类的静态字段(被 final
修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为
REF_getStatic, REF_putStatic, REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
2. 被动引用以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
通过子类引用父类的静态字段,不会导致子类初始化。
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
SuperClass[] sca = new SuperClass[10];
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
System.out.println(ConstClass.HELLOWORLD);
类与类加载器两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
类加载器分类从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分; 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 \lib
目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如
rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java
程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 扩展类加载器(Extension ClassLoader)这个类加载器是由
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将
/lib/ext 或者被 java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 应用程序类加载器(Application ClassLoader)这个类加载器是由
AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是
ClassLoader 中的 getSystemClassLoader()
方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型 应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。 下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation
Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
1. 工作过程一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
2. 好处使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到
ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的
Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object
使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
3. 实现以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass()
方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出
ClassNotFoundException,此时尝试自己去加载。
public abstract class ClassLoader {
// The parent class loader for delegation
private final ClassLoader parent ;
public Class <?> loadClass ( String name ) throws ClassNotFoundException {
return loadClass ( name , false );
}
protected Class <?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException {
synchronized ( getClassLoadingLock ( name )) {
// First, check if the class has already been loaded
Class <?> c = findLoadedClass ( name );
if ( c == null ) {
try {
if ( parent != null ) {
c = parent . loadClass ( name , false );
} else {
c = findBootstrapClassOrNull ( name );
}
} catch ( ClassNotFoundException e ) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if ( c == null ) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass ( name );
}
}
if ( resolve ) {
resolveClass ( c );
}
return c ;
}
}
protected Class <?> findClass ( String name ) throws ClassNotFoundException {
throw new ClassNotFoundException ( name );
}
}
自定义类加载器实现以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自
java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class
文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。 java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
public class FileSystemClassLoader extends ClassLoader {
private String rootDir ;
public FileSystemClassLoader ( String rootDir ) {
this . rootDir = rootDir ;
}
protected Class <?> findClass ( String name ) throws ClassNotFoundException {
byte [] classData = getClassData ( name );
if ( classData == null ) {
throw new ClassNotFoundException ();
} else {
return defineClass ( name , classData , 0 , classData . length );
}
}
private byte [] getClassData ( String className ) {
String path = classNameToPath ( className );
try {
InputStream ins = new FileInputStream ( path );
ByteArrayOutputStream baos = new ByteArrayOutputStream ();
int bufferSize = 4096 ;
byte [] buffer = new byte [ bufferSize ];
int bytesNumRead ;
while (( bytesNumRead = ins . read ( buffer )) != - 1 ) {
baos . write ( buffer , 0 , bytesNumRead );
}
return baos . toByteArray ();
} catch ( IOException e ) {
e . printStackTrace ();
}
return null ;
}
private String classNameToPath ( String className ) {
return rootDir + File . separatorChar
+ className . replace ( '.' , File . separatorChar ) + ".class" ;
}
}
参考资料