JVM内存模型


JVM运行时,内存区域划分为:

  • 堆:线程共享。所有对象的实例都在堆上分配内存。堆区分为年轻代和年老代,其比例为1:2。年轻代中又分为eden区和s0、s1,其比例为8:1:1。
  • 栈:线程私有。一个每个方法执行时会创建一个栈帧,方法调用就对应着出入栈。每个栈帧包含局部变量表、操作数栈、动态连接、返回地址。
    局部变量表用于存储方法参数和局部变量。
    操作数栈用于把字节码指令传到操作数栈,以及接收方法返回结果。
    动态连接用于把符号引用的方法,改为实际方法的直接引用。
  • 元空间:在jdk1.7之前,有方法区的概念(也叫永久代)。常量池就位于方法区。jdk1.7之后,方法区被取消,常量池被移到了堆中。并新增了元空间,用于存放运行时常量池和Class文件。
  • 程序计数器:线程私有。用于记录当前线程下虚拟机正在执行的字节码的指令地址。
  • 本地方法栈:线程私有。用于执行本地native方法。



GC


定义

Java CG即java的垃圾回收机制。在java中一般不需要开发者自行编码去进行垃圾清除和内存回收。

GC算法

复制算法

把内存分为两块。先遍历使用过的一块,并标记出可以回收的对象。然后把这些对象清除,之后再把存活的对象复制到另一块内存中。

实际上,年轻代上eden区和s0、s1区的比例是8:1:1。每次只使用eden区和其中一个survivor区,只空着一个survivor区,这样不会浪费太多的内存空间。

当存活的对象在s0和s1间来回复制超过一定次数(默认15次)后,就会进入年老代。或者,survivor区中超过一半的对象年龄相同,则如果有对象的年龄超过这些对象,则直接进入年老代。

标记清除

遍历一遍,标记出可以回收的对象,然后清除这些对象。但是会带来内存碎片的问题。

标记压缩

遍历一遍,标记出存活的对象,然后把对象压缩到内存的一端,再回收另一端的对象,防止内存碎片的出现。

判断对象是否可回收

即主要是看该对象有没有被引用。有两种方法:

  • 引用计数法:即对象每当被引用,就在对象的引用计数器上+1,否则就-1。当计数器为0时,说明对象没有被引用,可以回收。但是引用计数法会出现循环引用的问题。
  • 引用链法:找到GC ROOT对象,从GC ROOT对象开始,往下搜索,这个路径就是引用链。如果一个对象到CG ROOT对象没有引用链,则说明对象可以回收。在Java GC中就是用这种方法判断。

CG ROOT对象一般包括:

  • 栈中引用的对象。
  • 静态变量和常量引用的对象。
  • 本地方法栈native方法引用的对象。



垃圾回收器


在年轻代上,使用的都是复制算法,用到的回收器有:

  • serial:串行回收器。单线程进行垃圾回收,回收的时候会stop the world(即暂停所有工作线程)。
  • parallel:并行回收器。多线程进行垃圾回收。
  • parNew:serial的多线程版本。

在年老代上,使用的是标记清除和标记压缩算法,用到的垃圾回收器有:

  • serial old:串行回收器的年老代版本。
  • parallel old:并行回收器的年老代版本。
  • cms:是一个以获取最短停顿时间为目标的收集器。它的GC过程分为4步。
    1.标记GC ROOT关联的对象。
    2.并发标记所有可回收对象。
    3.修正并发标记期间的标记。
    4.并发清除对象。

G1垃圾回收器,是JDK9后的默认垃圾回收器。它可以跟用户程序并发执行回收。它将堆划分为多个大小相等的region,不再分年老代和年轻代。G1回收器在停顿和延迟可控情况下尽可能提高了吞吐量。



YGC和FullGC


当一个对象在堆中请求内存分配时,eden区无法满足内存分配的需求,则触发ygc。如果ygc后,内存仍然不足,则对象进入年老代。年老代也无法分配空间,则触发fullgc。如果fullgc也不能满足所需空间,则抛出oom异常。



类加载


类加载器

  • Bootstrap类加载器:即启动类加载器,负责加载jdk中rt.jar中的类文件。它是所有类加载类的父类加载器。
  • Extension类加载器:即扩展类加载器,负责加载java的扩展类库,即/jre/lib/ext下的类文件。
  • System类加载器:即系统类加载器,负责把用户类路径(即java的classpath或-Djava.class.path变量所指的目录,也就是当前类所在路径及其引用的第三方类库的路径)下的类文件。
  • 自定义类加载器。

双亲委派模型

当一个类加载的时候,首先会向上询问父类加载器是否已经加载,如果没有则依次往上询问。如果父类加载器都没有加载,则从上往下依次尝试当前类加载器是否能加载。

逆双亲委派模型

即不按照双亲委派模型的顺序去进行加载。比如jndi,它的代码由启动类加载器加载。但jndi需要调用classpath下的jndi接口。由于启动类加载器不知道这些代码,所以就需要通过thread类的setContextClassLoader设置线程上下文类加载器(即thread context classloader),用这个父类加载器去请求子类加载器完成加载。

类的生命周期

  • 加载:将字节码转为二进制流。
  • 验证:验证该二进制文件是否符合class文件规范。
  • 准备:为静态变量、常量赋值。
  • 解析:把常量池中的符号引用改成直接引用。
  • 初始化:执行初始化代码。

类对象的实例化过程

  • 判断类是否加载。如果未加载,则需要先执行类加载。
  • 类加载后,首先需要分布内存空间。
  • 接着,为实例赋默认值。
  • 设置对象头,包括hashcode,cg年龄等。
  • 执行构造函数初始化。