Java虚拟机

  • 2020 年 9 月 22 日
  • 笔记

虚拟机内存划分

  1. 程序计数器

计数器记录了正在执行的虚拟机字节码指令的地址,如果执行的是native方法,计数器值为空

  1. Java虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机中入栈到出栈的过程虚拟机栈不可以动态扩展时,如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError异常。虚拟机栈动态扩展时,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

  1. 本地方法栈

与虚拟机栈相似,不过对应的是native方法服务,同样会抛出上述异常。

  1. Java堆

存放对象实例,逻辑上是连续的,物理上 不一定连续,可通过-Xmx和-Xms扩展

  1. 方法区

用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

  1. 运行时常量池

用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法去的运行时常量池中存放,以及string常量,常用于intern方法(当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。)


对象的创建过程(p45)

虚拟机遇到一个new指令后,首先检查该指令参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载,解析初始化,如果没有则执行类加载过程。

对象的访问定位(Java程序需要通过栈上的reference数据来操作堆上的具体对象)
  1. 句柄方式

Java堆中会划分出一块内存来作为句柄池,reference中存储了对象的句柄地址,而句柄包含了对象的具体地址。

  1. 直接指针访问

Reference中存储了对象的的真实地址

优劣:对象被移动只会改变句柄钟到实例数据指针,reference不需要修改,直接指针速度快。


垃圾回收算法

  1. 引用计数算法

对象保存一个引用计数器,每当有一个地方引用它时,计数器值加一,引用是失效时则减一,为0则不在被使用,缺点是难以解决循环引用的问题。

  1. 可达性分析算法

通过称为“GC root”的对象为起始点,从这些起始点向下搜索,当一个对象到GC root 没有任何引用链相连,则这个对象是不可达的,在枚举根节点分析时保证对象引用关系不变化,所以会造成停顿。

GC root对象:

a. 虚拟机栈中引用的对象

b. 方法区中静态变量属性引用的对象

c. 方法区中常量引用的对象

d. 本地方法栈中引用的对象

  1. 标记清除算法

标记完后续统一回收,但标记清除效率不高,会产生大量不连续的内存碎片,该算法是GC的基础。

  1. 复制算法

Java中将堆分为新生代和老年代,新生代又分为Eden和两个servivor空间,每次将存活对象复制到另一个servivor中,如果servivor空间不足,则由老年代进行分配担保,存储这些对象。

  1. 标记整理算法

该算法工作在老年代,在标记后将存活对象向一端移动,清理另一边的内存,以应对对象存活率较高的情况。

  1. 分代收集算法

即将堆分为新生代和老年代,再在不同代的堆运行不同的算法,新生代采用复制算法,老年代使用标记清除或者标记整理算法。


对象回收过程

首先进行可达性分析,这里会造成GC停顿,停顿指的是线程执行到安全点停顿,如果对象不可达,则开始第一次标记筛选,筛选条件是是否有必要执行finalize方法,如果覆盖了finalize方法,且未被执行过,则将对象放置在F-Queue队列中,稍后GC会对该队列进行第二次标记并执行finalize方法,在finalize方法中没有将自身引用赋值给其他变量,则回收。

垃圾收集器

Serial收集器

单线程收集器,工作在新生代,进行垃圾收集时,必须暂停其他线程,在新生代采取复制算法

ParNew收集器

Serial收集器的多线程版本,该收集器多个GC线程并行执行,但同样需要暂停用户线程,能与CMS收集器配合各工作,因CMS只能和ParNew或者Serial收集器配合工作,所以该收集器时Server模式首选收集器。

Parallel Scavenge收集器

新生代收集器,采用复制算法,该收集器的特点是吞吐量可控制,即CPU用户线程运行的时间与总时间的比值

Serial Old收集器

Serial收集器的老年代版本,采用标记整理算法,主要给client模式下的虚拟机使用,单线程。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,采用标记整理算法,多线程

CMS收集器

工作在老年代,以获取最短回收停顿时间为目标的收集器,响应速度快,用户体验好。基于标记清除算法,分为四步:初始标记仅标记下GC Root直接关联的对象,需要暂停用户线程;并发标记不用暂停用户线程;重新标记修正并发标记时标记变动的那部分对象,需要暂停用户线程;最后时并发清除,不需要暂停用户线程。

G1收集器

地表最强收集器,堆不再划分成新生代和老年代,而是一块块大小相等的内存区域,G1会根据小堆的垃圾占比进行有优先级的区域回收方式。G1收集器分为四个步骤;初始标记,并发标记,最终标记,筛选回收。初始标记仅标记GC Roots直接关联的对象,需停顿用户线程;并发标记与用户线程并发执行,对堆中对象进行可达性分析,找出存活对象;最终标记修正并行标记阶段标记产生变化的记录,可并行执行,但需停顿用户线程;筛选回收则清理不可用对象,可与用户线程并发执行,但一般停顿用户线程效率更高


Minor GC和Full GC的区别

Minor GC发生在新生代,比较频繁,Full GC发生在老年代,速度慢。堆中内存不足会触发GC,要避免老年代的GC。

对象进入老年代的时机

大对象直接进入老年代,所以大对象存活时间又短的容易触发Full GC;Minor GC时,survivor空间不足,对象因分配担保进入老年代;对象保存有年龄计数器,每进行一次Minor GC,年龄加一,到了阈值会进入老年代;动态年龄判定,在survivor空间中相同年龄所有对象大于survivor的一半,该年龄以上的对象进入老年代。

类加载的时机
  1. 使用new关键字实例化时,读取或设置一个类的静态字段时(对于final修饰的类常量,在调用时并不会触发被调用类的初始化,因为该常量已被编译到调用类的常量池字节码中),调用一个类的静态方法时。

  2. 使用java.lang.reflect包的方法对类进行反射调用时,类未被初始化。

  3. 初始化一个类时,父类没有被初始化,需先初始化父类(接口不要求父接口已初始化,除非用到了父类接口)。

  4. 虚拟机启动时初始化mian方法主类。

  5. 使用动态语言支持时。

除此之外,其他任何情况都不会触发类的初始化,如通过子类调用父类的静态字段,不会初始化子类。


类加载过程
  1. 加载

将类加载进方法区,生成代表该类的class对象,作为方法区该类的各种数据入口。

  1. 验证

验证字节流是否符合class文件规范,确保class文件的字节流符合当前虚拟机规范。

  1. 准备

为类变量分配内存并设置类变量初始值,除final类型的变量,其余都赋值为零或null或false。

  1. 解析

将符号引用替换为直接引用

  1. 初始化

执行类中Java代码,将初始值替换为真实赋值。

注意:初始化时代码执行是从上往下执行的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的类变量,只能赋值(在准备阶段已经分配了内存),不能访问(还没有赋真值,访问的值是无效的,这就不安全了)。

子类和父类初始化顺序:

1,在类加载的时候执行父类的static代码块,并且只执行一次(因为类只加载一次);

2,执行子类的static代码块,并且只执行一次(因为类只加载一次);

3,执行父类的类成员初始化,并且是从上往下按出现顺序执行;

4,执行父类的构造函数;

5,执行子类的类成员初始化,并且是从上往下按出现顺序执行。

6,执行子类的构造函数。


双亲委派模型

Java中类加载器可分为三类:

  1. 启动类加载器

使用c++语言实现,负责加载lib目录中的类库,无法被Java程序直接引用。

  1. 扩展类加载器

使用Java实现,负责加载lib\ext目录中的类库,开发之可以直接使用。

  1. 应用程序类加载器

由ClassLoader实现,负责加载用户类路径(classpath)上指定的类库,开发者可以直接使用。

双亲委派模型的工作过程:如果一个类收到了类加载的请求,会首先把这个请求委派给父类加载器(使用组合关系来复用父加载器的代码),因此所有的加载请求都委派到顶层的启动类加载器,只有父类加载器无法完成这个加载请求,子加载器才会尝试自己去加载,这样保证了object类在程序中都是同一个类,保证了程序的稳定。