Stay foolish, stay hungry.

Java虚拟机纲要

Posted on By 吴杰

Java内存模型

##运行时数据区域

运行时数据区域包含方法区、堆、虚拟机栈、本地方法栈以及程序计数器

程序计数器

每个线程有一个独立的程序计数器,用于保存当前程序的执行状态。字节码解释器工作时通过改变计数器的值来选取吓一跳需要执行的指令,分支、循环、跳转、异常处理、线程恢复等都依赖这个计数器。

执行Native方法时程序计数器的值为空(Undefined)。

Java虚拟机栈

同样是线程私有。虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口灯信息,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程。

本地方法栈

与虚拟机栈类似,不过只为Native方法服务。

Java堆

是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也叫“GC堆”(Garbage Collector Heap)。

方法区

是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也叫做Non-heap,和堆区区分开来。

该区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

垃圾回收机制

判断对象需要回收的方法

引用计数器法

给每一个对象分配一个计数器,记录有多少个地方引用该对象,当该计数器为0时即回收该对象。

优点为实现简单,缺点为无法解决相互引用的问题。

可达性分析算法

主流编程语言中通用的方法。

通过一系列的GC Roots作为起始点(根)向下搜索,搜索路径被称为引用链(Reference chain),当一个对象到GC Roots没有任何引用链相连接,则回收该对象。

GC Roots包括:

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

引用的类型

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分成了:

  • 强引用 Strong Reference
  • 软引用 Soft Reference
  • 弱引用 Weak Reference
  • 虚引用 Phantom Reference

强引用

类似Object obj = new Object(),只要强引用还在,就永远不会被回收。

软引用

通过SoftReference类来使用软引用。在系统即将要发生内存溢出之前,会将只被软引用的对象回收。

弱引用

通过WeakReference类来使用弱引用。在下一次垃圾回收的时候,一定会回收掉只被弱引用的对象。

虚引用

通过PhantomReference类来使用虚引用。对于垃圾回收来说,虚引用等于没有引用,唯一目的就是在回收的时候会收到一个系统通知。

垃圾回收算法

标记-清除算法

最基础的垃圾收集算法,先标记出所有需要回收的对象,在标记完成后统一回收这些对象。

这个算法的不足点有:

  • 效率低,无论是标记过程还是清除过程
  • 空间利用率不高,因为标记清楚之后会产生大量不连续的内存碎片

复制算法

将可用内存分成容量相等的两块,先使用一块,这块用完了就把这一块上面仍然存活的对象拷贝到另一块(未使用的内存)上,同时一次性清理之前的使用过的内存。

优点是效率比标记-清除算法高;缺点是只能使用一半的内存空间。

现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是按照1:1来分配空间的,而是分成了一块较大的Eden空间和两块较小的Survivor空间,因为存活的对象一般比较少。每次使用Eden和一块Survivor,用完了将他们上面的存活对象拷贝到另一个Survivor上,清除这两块空间。

标记-整理算法

如果对象存活率较高的话,就不适合使用复制算法了。

标记-整理算法和标记-清除算法类似,不过不是直接清除被标记的内存,而是先把存活的对象向内存一端移动(整理),然后清除除此之外的内存空间。

分代收集算法

这并不是一个新的收集算法,而是根据不同内存的情况来分别处理。当前的商业虚拟机的垃圾收集都采用这一算法。

一般是把Java堆分成新生代和老年代,在新生代中每次收集时都有大量对象死去,适合用复制算法;老年代中存活率较高,使用标记-清除算法或者标记-整理算法。

内存分配策略

GC分为新生代GC和老年代GC。

新生代GC(Minor GC)是发生在新生代的垃圾收集,因为大多数Java对象都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC),发生在老年代,速度一般比Minor GC慢10倍以上。

常用的内存分配策略有:

  • 对象优先在Eden分配
  • 大对象直接进入老年代,比如长字符串以及数组
  • 长期存活的对象进入老年代。每一次Minor GC都会记录和修改存活对象的年龄,年龄足够大(可以通过MaxTenuringThreshold设置)的对象就会进入老年代。
  • 动态对象年龄判定,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保。

类加载机制

类从加载到卸载经过的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。验证、准备和解析统称为连接。

Process of Class Loading

类加载的时机

开始类加载过程第一个阶段“加载”的时机有:

  1. 使用new实例化对象的时候、读取或设置一个类的静态字段的时候,以及调用一个类的静态方法的时候;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则先要触发其初始化;
  3. 当初始化一个类的时候,如果父类没有初始化,则先初始化父类;
  4. 虚拟机启动时,需要指定一个包含main()方法的主类,虚拟机会先初始化这个主类;
  5. 如果一个java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则先触发其初始化。(?)
package org.fenixsoft.classloading;
/***被动使用类字段演示一:*通过子类引用父类的静态字段,不会导致子类初始化**/
public class SuperClass{
    static{
        System.out.println("SuperClassinit!"
    }
    public static int value=123;
}
public class SubClass extends SuperClass{
    static{
        System.out.println("SubClassinit!");
    }
}

public class ConstClass{
    static{
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWORLD="hello world";
}

public class NotInitialization{
    public static void main(String[]args){
        System.out.println(SubClass.value); //1
        SuperClass[] sca = new SuperClass[10]; //2
        System.out.println(ConstClass.HELLOWORLD); //3
    }
}

假设上面的函数main()中的语句每次只执行其中一条。

  1. 只会输出“SuperClass init!”,而不会输出“SubClass init!”,因为对于静态字段,只有直接定义这个字段的类才会被初始化。SubClass.value中调用的value是父类中定义的,所以按照情况1会初始化SuperClass。
  2. 不会触发SuperClass的初始化,因为通过数组定义来引用类,不会触发此类的初始化。
  3. 不会触发ConstClass的初始化,因为经过传播优化,常量HELLOWORLD的值在编译阶段就存储到了NotInitialization类出常量池中。所有对ConstClass.HELLOWORLD的引用实际都转化成了NotInitialization自身常量池的引用。

类加载的过程

加载

加载阶段要完成的工作为:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法去的运行时数据;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

数组类本身不通过类加载器创建,而是有Java虚拟机直接创建。但是数组类中的Element Type最终是要用类加载器创建的。

验证

验证的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

大致上,验证包含4个检验步骤:

  1. 文件格式验证
  2. 元数据验证:对字节码描述的信息进行语义分析,保证不存在不符合Java语言规范的元数据信息(主要是数据类型信息)。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。主要是对方法体进行校验。
  4. 符号引用验证:确保解析动作能正常执行,如果无法通过符号引用验证,可能会抛出NoSuchFieldError、NoSuchMethodError等

准备

正式为变量(类变量,即static变量)分配内存并设置变量初始值,这些变量所使用的内存都将在方法区中进行分配。

注意:准备阶段只是赋初始值,而不是定义的值,比如public static int value = 123;在准备过后value的值是0而不是123,不同类型的初始值为:

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

解析

虚拟机将常量池内的符号引用替换为直接引用

初始化

根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以说,初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

双亲委派模型