Fork me on GitHub

Java虚拟机笔记

内存模型以及分区

JVM所管理的内存分为以下几个运行时数据区:

  • 程序计数器:当前线程所执行的字节码的行号指示器。作用:为了线程切换后能恢复到正确的执行位置

  • Java虚拟机栈:描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,存储局部变量表、操作数栈、动态链表、方法出口等信息。局部变量表存放编译期可知的各种基本数据类型、对象引用和returnAddress类型。局部变量表的内存空间大小在编译期间确定,之后不再更改。

  • 本地方法栈:为虚拟机使用到的Native方法服务。

  • Java堆:唯一目的:存放对象实例,所有的对象实例以及数组都要在堆上分配。堆可细分为新生代和老年代,进一步细分为Eden区、From Survivor区、To Survivor区,提高效率(TLAB)

  • 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据

  • 运行时常量池:存放编译期生成的各种字面量和符号引用

对象的创建

  1. 虚拟机遇到一条new指令时
  2. 先检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过
  3. 如果没有,就先执行类加载过程
  4. 类加载检查通过后,虚拟机将为新生对象分配内存
  5. 分配内存方法:指针碰撞(内存工整)和空闲列表(内存不工整)
  6. 内存分配之后,将分配到的内存空间都初始化为零值
  7. 虚拟机对对象进行必要的设置
  8. 执行< init>方法,把对象按照程序员的意愿进行初始化

对象的内存布局

对象在内存中存储的布局可分为3块区域:对象头、实例数据和对齐填充

  • 对象头 包括用于存储对象自身的运行时数据、类型指针(对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例)
  • 实例数据 对象真正储存的有效信息,也是在程序代码中所定义的各种类型的字段内容
  • 对齐填充(占位符)

对象的访问定位

  1. 句柄,Java堆中将会划分出一块内存来作为句柄池,reference中储存的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
  2. 直接指针,Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址

区别:
使用句柄来访问的最大好处是: reference中储存的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改
使用直接指针访问方式最大的好处是: 速度更快,节省了一次指针定位的时间开销

对象已死?

  • 引用计数法:给对象添加一个引用计数器,有地方引用就+1,引用失效就-1,为0就不可能被在使用。无法解决对象之间相互循环引用的问题。

  • 可达性分析法:对象到 GC Roots没有任何引用链相连时,此对象是不可用的。

四种引用

  • 强引用:垃圾回收器不会回收
  • 软引用:描述一些还有用但并非必须的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
  • 弱引用:描述非必需对象,只能生存到下一次垃圾收集发生之前
  • 虚引用:能在这个对象被回收器回收时收到一个系统通知

确定死亡

两次标记过程

  • 可达性分析之后确定对象不可用,再进行一次筛选。筛选条件是是否有必要执行finalize()方法(对象没有覆盖finalize或finalize方法以及被调用过,那么就没有必要执行;有必要执行finalize()方法,对象就会放置在F-Queue队列中,然后虚拟机”会去finalize”它)

垃圾收集算法

  • 标记-清除算法
    先标记需要回收的对象,标记完成后统一回收所有被标记的对象。
    不足:效率问题和空间问题

  • 复制算法
    先划分容量相等的两块,每次使用其中一块,当其中一块内存用完,就将还存活的对象复制到另一块上面,然后把使用过的内存清理
    优点:实现简单、运行高效
    缺点:内存缩小一半

  • 标记-整理算法
    先标记,然后让所有存活的对象都向一端移动,然后清理掉边界以外的内存

垃圾收集器

  • Serial收集器

  • ParNew 收集器

  • Parallel Scavenge收集器

  • Serial Old收集器

  • Parallel Old收集器

  • CMS收集器

  • G1收集器

堆里面的分区:Eden,survival from to,老年代,各自的特点。

  • Eden:Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。
    而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。
    在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。

  • Survival from to:在Java堆的年轻代,Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,
    在发生一次Minor GC后,from区就会和to区互换。
    在发生Minor GC时,Eden区和Survival from区会把一些仍然存活的对象复制进Survival to区,并清除内存。
    Survival to区会把一些存活得足够久的对象移至年老代。

  • 老年代:存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。
    当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源

空间分配担保

大意就是发生Minor GC之前先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,成立的话,Minor GC就安全;不然就去检查是否允许担保失败。

允许的话就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小?

大于的话,将尝试进行一次Minor GC,尽管是有风险的;如果小于就改为进行一次Full GC

虚拟机类加载机制

虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

五种情况必须立即对类进行”初始化”

  1. 遇到 new getstatic putstatic 或 invokestatic 这4条指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的场景:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已经在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main 函数的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic REF_putStatic REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
  • 加载
    完成三件事情:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
  • 验证
    目的:为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
    完成四个阶段的校验:文件格式验证、元数据验证、字节码验证和符号引用验证

  • 准备
    正式为变量分配内存并设置类变量初始值的阶段,这些变量所使用的的内存都将在方法区中
    注意

    1. 这个时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
    2. 这里所说的初始值”通常情况”下是数据类型的零值。但是如果类字段的字段属性表中存在 ConstantValue 属性,那么在准备阶段变量 value 就会被初始化为 ConstantValue 属性指定的值。例 public static final int value = 123
  • 解析
    解析阶段就是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 初始化
    初始化阶段真正开始执行类中定义的Java程序代码
    在准备阶段,变量已经赋过一次系统要求的初始值,在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
    父类中定义的静态语句块要优先于子类的变量赋值操作

分派:静态分派与动态分派

  • 静态分派与重载有关,虚拟机在重载时是通过参数的静态类型,而不是运行时的实际类型作为判定依据的;静态类型在编译期是可知的;

  • 动态分派与重写(Override)相关,invokevirtual(调用实例方法)指令执行的第一步就是在运行期确定接收者的实际类型,根据实际类型进行方法调用

双亲委派模型

工作过程
如果一个类加载器收到了类加载的请求,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。