Java虚拟机二

  • 三、内存分配与回收策略
  • Minor GC 和 Full GC
  • 内存分配策略
  • 1. 对象优先在 Eden 分配
  • 2. 大对象直接进入老年代
  • 3.长期存活的对象进入老年代
  • 4.动态对象年龄判定
  • 5.空间分配担保
  • Full GC 的触发条件
  • 1. 调用 System.gc()
  • 2.老年代空间不足
  • 3. 空间分配担保失败
  • 4. JDK 1.7 及以前的永久代空间不足
  • 5. Concurrent Mode Failure
  • 四、类加载机制
  • 类的生命周期
  • 类加载过程
  • 1. 加载
  • 2. 验证
  • 3. 准备
  • 4.解析
  • 5. 初始化
  • 类初始化时机
  • 类与类加载器
  • 类加载器分类
  • 双亲委派模型


三、内存分配与回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存分配策略

1. 对象优先在 Eden 分配
  • 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC
2. 大对象直接进入老年代
  • 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
  • -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
3.长期存活的对象进入老年代
  • 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1岁,增加到一定年龄则移动到老年代中。
  • -XX:MaxTenuringThreshold 用来定义年龄的阈值。
4.动态对象年龄判定
  • 虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。
5.空间分配担保
  1. 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC可以确认是安全的。
  2. 如果不成立的话虚拟机会查看 HandlePromotionFailure的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()
  • 只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2.老年代空间不足
  • 老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。
  • 为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3. 空间分配担保失败
  • 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
4. JDK 1.7 及以前的永久代空间不足
  • 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class的信息、常量、静态变量等数据。
  • 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 FullGC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
  • 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5. Concurrent Mode Failure
  • 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure 错误,并触发 Full GC。

四、类加载机制

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

类的生命周期

java虚拟机运行模式有哪些_初始化

类加载过程

类加载过程:
1、类加载就是执行Java程序编译之后在字节码文件中生成的clinit()方法(称之为类构造器),clinit()方法由静态变量和静态代码块组成。
2、子类的加载首先需要先加载父类,如果父类为接口。则不会调用父类的clinit方法。一个类中可以没有clinit方法。
3、clinit方法中的执行顺序为:父类静态变量初始化,父类静态代码块,子类静态变量初始化,子类静态代码块。
4、clinit()方法只执行一次。

1. 加载

加载是类加载的一个阶段,加载过程完成以下三件事:

  • 通过类的完全限定名称获取定义该类的二进制字节流。
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
  • 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
    其中二进制字节流可以从以下方式中获取:
  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
2. 验证

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

3. 准备

类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存;实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

java虚拟机运行模式有哪些_java虚拟机运行模式有哪些_02

4.解析

将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

5. 初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 clinit() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源
clinit() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

由于父类的 clinit()方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

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
}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 clinit() 方法。但接口与类不同的是,执行接口的 clinit() 方法不需要先执行父接口的 clinit() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 clinit() 方法。
虚拟机会保证一个类的 clinit() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 clinit() 方法,其它线程都会阻塞等待,直到活动线程执行 clinit() 方法完毕。如果在一个类的 clinit() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

类初始化时机

1.主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

java虚拟机运行模式有哪些_java虚拟机运行模式有哪些_03


2. 被动引用

所有引用类的方式都不会触发初始化,称为被动引用

java虚拟机运行模式有哪些_java虚拟机运行模式有哪些_04

类与类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

类加载器分类

java虚拟机运行模式有哪些_java虚拟机运行模式有哪些_05

双亲委派模型

应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。

下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。

java虚拟机运行模式有哪些_java虚拟机运行模式有哪些_06


java虚拟机运行模式有哪些_java_07


自定义类加载器实现

继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。

java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。