JVM复习笔记

JVM笔记

1. 概述

JVM主要由三个子系统组成

  • 类加载子系统
  • 运行时数据区
  • 执行引擎
    JVM复习笔记

2. java内存区域

2.1. 区域划分

JVM复习笔记
JVM复习笔记

  • 程序计数器
    • 可以理解为是用来标记程序执行的行号。这个行号便于流程控制,比如分支,跳转,中断等。显然每个线程都应该拥有一个程序计数器,不然每个线程之间都会打架。
  • 方法区(+运行时常量池(1.7之前))
    • 方法区是线程共享的,其中保存了类的基本信息,包括类信息,常量,类变量,即时编译器编译后的代码等数据。
    • 运行时常量池
  • 虚拟机栈
    • 栈是线程私有的,与线程同生共死。栈中有栈帧,一个栈帧可以认为是一个方法。栈帧中包括局部变量表,操作数栈,动态链接和方法出口。
    • 局部变量表包括基本类型和对象引用
  • 本地方法栈
    • 虚拟机栈调用java方法,本地方法栈调用本地方法,即native方法服务。在HotSpot中,合二为一,本地方法栈也会创建一个栈帧。
  • 堆(+运行时常量区,1.7之后)
    • 用于存放对方实例。
      JVM复习笔记
      1.8中取消了永久代,转而使用元空间。元空间和永久带的本质区别是,元空间不使用虚拟机内存,而使用物理内存。
      • GC不需要考虑元空间。
      • 类及方法需要空间难以确定大小,所以干脆不确定大小。
      • 字符串存储在永久带中不爽,会有性能问题和内存溢出。1.7以后String也放在堆上了。
    • 运行时常量池
      JVM复习笔记
  • 永久代与方法区的关系
    • JAVA1.6之前,通过在永久代中实现方法区功能,好处是可以用GC来管理这部分内存。
    • 1.8中移除了永久带,因此在元空间中实现方法区功能。
    • 1.8后,运行时常量池移动到了堆中,原来在永久代里,String就在这里,所以String现在在堆里。
  • OOM问题,及Out of Memory
    • 栈也是在内存中的,如果栈的大小可扩展但是物理内存不够了,会抛出oom。本地方法栈也一样。此外,栈会报错栈溢出错误。
    • 堆无法再扩展,oom
    • 方法区无法满足内存分配需求,oom
    • 运行时常量池在方法区中,方法区在元空间中,即在本机内存中,也会oom
    • 直接内存,收到主机物理内存限制,oom

2.2. 对象创建

  • 创建流程
    JVM复习笔记
    • 类加载检查。检查常量池中是否有对应的符号引用,比如com.sonihr.dao.User,能查到对应的符号引用,再去检查其是否已经被加载,解析和初始化过。如果没有,就必须先执行相应的类加载过程。
    • 分配内存。对象所需的内存在类加载完成后便可以完全确定。
      • 指针碰撞 如果内存规整则使用指针碰撞,原理就是堆内存向后划分一段区域
        • 适合Serial、ParNew等带Compact过程的收集器。
        • 复制算法内存也是规整的。
      • 空闲列表 如果内存不规整,则从可用列表中选择一块足够大的空间
        • 适合CMS这种标记清除的收集器。
    • 并发创建对象。
      • CAS+失败重试,保证创建对象的原子性。
      • 在堆中预先分配一小块内存,TLAB(Thread Local Alloction Buffer,线程本地分配缓冲),每个线程都有,不够再去向堆中申请内存。
    • 初始化0值。保证java中的对象不需要赋初值也能够直接使用。但是基本对象不行。
    • 设置对象头。设置关于对象的一些信息,可以理解成是这个对象的户口本,包括是那个类的实例,如何找到类的原数据信息,哈希码,GC分代年龄等。
    • 执行init方法。对用户而言,这里才是我们真正熟悉的地方。init方法中会运行静态方法块,构造方法等。

2.3. 对象的内存布局

对象的布局可以分为3块区域:对象头,实例数据和对齐填充。

  • 对象头(Mark Word+类型指针)
    • mark word 用于存储对象自身运行时的数据。包括哈希码,GC分代年龄,锁状态,线程持有锁,偏向线程ID,偏向时间戳等。
    • 类型指针。虚拟机通过这个指针来确定这个对象是哪个类的实例。但是,查找对象的原数据信息不一定要经过对象本身。
  • 实例数据。存放对象的各种field内容。
  • 对齐填充。对象的大小必须是8字节的整数倍。

2.4. 对象的访问定位

java通过栈上的引用来指向实际堆中的对象实例,至于如何引用,有不同的方式。

  • 句柄。引用保存句柄地址,句柄中保存到对象实例数据的指针和到对象类型数据(非对象类信息,存储在方法区中的那部分)的指针。优点是当对象实例数据地址改变时,句柄不必改变,即引用也不必改变。
    JVM复习笔记
  • 指针。引用直接保存实例数据地址,实例对象存有对象类型数据。HotSpot采用这种方法,这也是为什么对象的内存布局中,对象头具有类型指针。优点是速度快。
    JVM复习笔记

2.5. 补充

Float与Double并未实现常量池技术。
Byte,Short,Character,Integer,Boolean在常量池中默认放置了[-128,127]的常量,可以直接引用,超出范围的要新建对象。

3. GC/JAVA垃圾回收

3.1. 灵魂的发问

  1. 哪些内存需要回收
  2. 什么时候回收
  3. 如何回收

3.2. 那些内存需要回收

  1. 堆中对象。
  2. 1.7之前的永久代中的常量。
  3. 1.7之前的永久代中的类。

3.3. 什么时候回收

  1. 对象
  • 引用计数法(C++智能指针)

    • 问题,循环引用,导致每个计数都不为0,但是其实已经没用了。C++采用的方案是通过weak_ptr让程序员手动解决,Java的解决方案是,干脆不采用引用计数这种方法。
  • 可达性分析算法

    JVM复习笔记

    • 如上图所示,object5,6,7显然是循环引用,但是没有与GC Roots相连,因此会被标记为可GC对象。
    • GC Roots
      • 虚拟机栈(栈帧中本地变量表)中引用的对象。
      • 方法区类属性引用的对象。
      • 方法区常量引用的对象。
      • 本地方法栈中JNI,即Native方法引用的对象。
      • 总结,本地/java栈里用的,类变量,类常量。
  • 再谈引用

    • 通过将引用分级来表示对这个引用的需求强度。
    • 强引用。只要强引用还存在,就绝对不可能回收。
    • 软引用。有用是有用,但是你要说删掉吧,也未尝不可,所以当出现内存溢出的风险时,会回收这部分内存。
    • 弱引用。基本没什么用,被弱引用关联的对象几乎是一次性用品。即只能工作到下一次GC之前。
    • 虚引用。唯一目的是当被引用对象被回收时能够收到一条系统通知。
  • Java的“析构函数”

    • finalize()
      如果对象覆盖了finalize()方法,那么可以在这里实现最后一次自我救赎,但是有且仅有一次,就算复活了,也仅有一次。当初设计这个方法的用意是为了在自动回收的时候可以做一些事情,而不是为了让他复活,因此对标的是C++中的析构函数作用。
  1. 常量
  • 如果没有任何引用指向常量,则回收。
  • 1.7后,运行时常量池已经在堆中。
  • 1.8后,这部分不会被GC处理。
  • 1.8之前,满足以下三种情况可以被定义为无用的类。
    • 没有任何实例。
    • 加载该类的类加载器已经被回收。
    • 该类的Class对象没有被引用,即无法在任何地方通过反射访问该方法。

3.4. 如何回收

JVM复习笔记

  1. 标记清除
    JVM复习笔记
  • 首先标记出需要回收的对象,然后统一清除。
  • 不足:1.标记和清除算法效率都不高。2.会产生大量不连续的内存碎片,如果以后需要较大连续区域,就必须提前触发一次GC。
  1. 复制算法
    JVM复习笔记
  • 为解决效率问题
  • 将内存氛围大小相同的两块,每次只使用一块,回收后将存货对象复制到另一半,然后将原本那一半清空。
  • 不足:浪费了一半的内存空间,牺牲太大。
  • 改进:实际使用中并不是等比例分配,而是默认8:1:1=Eden:Survivor1:Survivor2,Survivor采用的是复制的方法。因为一般来说,Eden中会有大量对象很快就死亡了,因此保持8:1的比例就可以保证存活下来的全部基本都可以放置到survivor中。但是总有例外,如果survivor的空间不能存在新生代中存活下来的对象,老年代就作为分配担保,这些对象将直接通过分配担保机制进入老年代。
  1. 标记整理
  • 复制算法如果要复制大量对象,效率就会变低。
  • 复制算法在老年代无法使用,因为老年代都是"老不死",什么大风大浪没见过?你复制来复制去,我都不死,还没有分配担保,就容易崩。
  • 标记整理的思想是不直接清理,而是将存活的对象向一段移动,然后清除掉其他。
  • 因为要频繁的移动内存位置,因此可以用句柄+句柄表实现。
    JVM复习笔记
  1. 分代收集
  • JVM复习笔记
  • 新生代因为1.朝生夕死2.内存占用1/3且存活少,用复制
  • 老年代用为老而不僵1.占用2/32.并且一般都是历经沧桑,不容易死,用标记整理/标记清除,避免大量内存复制

3.5. HotSpot的GC收集器

3.5.1. 枚举根节点

  • Stop the world 为了保证枚举根节点的时候,整个分析期间整个执行系统像被冻结了一样。
  • 采用OopMap数据结构,当类加载完成后,就会把对象内什么偏移量上是什么类型的数据计算出来,在JIT的过程中也会记录栈和寄存器的哪些位置是引用。换而言之就是会保存下来一些数据,Stop the world的时候可以便于读取。
  • 安全点。虚拟机只能在安全点才能启动GC。安全点的特征是长时间执行,比如方法调用,循环跳转,异常跳转。
  • 如何让所有线程一起到安全点?主动式中断,给定一个标志,各个线程去轮询这个标志,如果轮询到了就中断。
  • 安全区。如上文说到,主动式中断需要线程去轮询,但是被挂起或阻塞的线程根本都不会执行,那一辈子也到不了中断点啊。但是你思考一下,一个被阻塞的线程,需要GC么?不需要。应该对这些线程标记为安全区,即GC的时候不用考虑他们。

3.5.2. 垃圾收集器

  • JVM复习笔记
  • Serial
    • JVM复习笔记
    • 单线程,不仅说GC线程是单线程,更重要的是,做GC的时候别的不能干
    • 新生代采用复制Serial,老年代采用标记整理Serial Old。
    • 历史悠久,简单高效。高效的原因是不需要线程交互的开销,可以专心做垃圾收集。Client模式下的默认垃圾收集器。
  • ParNew
    • JVM复习笔记
    • Serial的多线程版本,这个多线程指的是多线程GC,但是GC和其他用户线程还是不能并发的
    • 新生代复制,老年代标记整理
    • 只有ParNew与Serial能和CMS组合使用。
    • 单核CPU用Serial,多核ParNew
  • Parallel Scavenge
    • 与ParNew的区别是,这是一个自动控制系统。即给定最优化目标值(吞吐量=(用户线程时间/(用户+GC时间))),然后虚拟机采用自适应调节策略进行优化。
    • 新生代复制。
  • Serial Old
    • 标记整理,专用老年代
    • 和Paraller Scanvenge搭配使用
    • CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。即,CMS说,兄弟,我并发收集失败了,你单线程收集兜底呗。Serial Old,好嘞兄弟,我来了。
  • Parallel Old
    • Parallel Scanvage的老年代版本。
    • 使用多线程,标记情理。
  • CMS
    • JVM复习笔记
    • 以回收停顿时间最短为目的
    • 基于标记清除
      • 初始标记 stop the world,只寻找和GC roots直接连接的对象
      • 并发标记 进行可达性分析,这个操作和用户线程是并发的
      • 重新标记 因为并发,导致部分对象标记可能出现错误,因此重新标记
      • 并发清除 对标记的对象进行清除
    • 明显缺点
      • 基于标记清除算法,因此会出现内存碎片。
        • 解决方法:当碎片过多即将开启full gc时,提前开启一个整理内存碎片的操作。
      • 因为是并发,因此对资源占用就比较高。特别是CPU资源,会用户而言会有影响。
        • 解决方法:采用增量式CMS,即减少单个线程占CPU时间,让用户线程和GC线程交替运行,让用户感觉速度下降的不明显。但是事实证明这个效果并不好。
      • 浮动垃圾。即在CMS无法清除所有垃圾,毕竟是并发的。本次产生的垃圾需要到下次GC才能清除,这就叫做浮动垃圾
        • 解决方法:预留出一部分空间给浮动垃圾。如果这个预留的值不够用,就会触发一次Serial Old进行老年代的垃圾清除。
  • G1收集器
    • JVM复习笔记
    • JVM复习笔记
    • 之前的所有收集器都是在物理层面分割了新生代和老年代,G1只从逻辑层面分割。
    • 避免对整个java堆的全区域扫描,而是跟踪各个区域(Region)的回收价值,在后台维护一个优先列表,优先回收价值大的。什么是回收价值呢?有的Region里垃圾对象多,那就是有价值的,有的Region里垃圾对象少,那就是没价值的。
    • 划分为新生代,老年代,巨型对象。
    • 整体来看是标记整理,两个区域之间为复制。
    • 对象的分配策略
      • Thread Local Allocation Buffer 线程本地分配缓冲区,为了对象能够快速分配出来。如果都在堆中分配,会需要同步机制。
      • Eden区
      • Humongous区分配
    • G1 young GC
      • Eden通过标记复制移动到Surivor中,如果Surivor中空间不够,则直接进入老年代。
      • 问题。如果仅仅GC新生代,那需要对堆中的全部内存进行可达性分析么?如果要,那相当于老年代中也要遍历一遍,会耗费大量的时间。于是G1采用RememberSet,(其他收集器中,也通过RememberSet来避免全堆引用,即处理新生代与老年代之间的关系。因为如果每次minor GC都要遍历一边老年代,而老年代通常比新生代大很多,这就非常耗费时间。
      • 如何避免遍历老年代?
        • JVM复习笔记
        • card table对应着一个byte[]。其中每个指向堆中连续的内存区域,cardtable[0]代表0到511字节,cardtable[1]代表512到1024字节。有什么用呢?每当写操作的时候,都会更新这个card table,凡是存在老年代对新生代的引用,就用dirty代表,即改变标志位的方式,说明引用。比如cardtable[1]=dirty,就说明512到1024字节中,有老年代对象对新生代对象存在引用,那么在GC Roots枚举的时候,就需要定位到这里,然后遍历这段区域的对象。这也叫做point-out,即老年代指向新生代。
          JVM复习笔记
        • 但是G1在此基础上更进一步!每个分区都分成若干个card。在每一个新生代上都有一个RememberedSet的数据结构,这个数据结构是一个Hash table,Key为指向他的region,values为这个region的cardtable的下标。
        • 举例说明,RegionA中有一个RememberedSet,key为RegionB,说明RegionB是老年代,并且这个老年代中有对象指向了它,是谁呢?就去查value里面的index,即可找到对应的地址.
    • G1 MIX GC
      • G1选择所有的新生代Region和部分收益高的老年代Rgion。
        • 如何判断收益高?global concurrent marking,全局并发标记。
          • 初始标记 stop the world,完成标记GC ROOTS直接可达的对象,并将他们压入扫描站等待后续扫描。
          • 并发标记。与应用程序并发执行,进行可达性分析。
          • 最终标记。Stop the world,比CMS更高效。TODO:为什么更高效
          • 清除。多线程复制清除。
        • 经过global concurrent marking之后,collector就知道哪些收益高了,然后根据用户指定的限制时间对Regions进行回收
          • young gc中CSet,即要被回收的集合是新生代中的region,通过控制region个数来控制GC。
          • mix gc中的CSet是全部的新生代和部分收益高的老年代。
        • G1延迟低的根本原因是回收的区域变得更小且更精准了,不像是原来只分新生代和老年代,现在细分更多,而且回收的粒度更小。
      • 如何保证在并发的时候,仍然可以确保存活的对象不被回收呢?
        • 类似MVCC的思想,快照读。在GC开始时刻创建一个对象图的快照(STAB,snapshot-at-the-beginning),但是会导致浮动内存垃圾。
    • G1的三色扫描算法
      • 黑色:根对象或者子对象已经全部被扫描
      • 灰色:自己已经被扫描,但还没有扫描子对象
      • 白色:未被扫描或者发现是垃圾对象
      • STAB用来解决并发情况下指针改变的问题。

3.5.3. 内存分配与回收策略

  • 对象优先在Eden分配,如果空间不够会进行一次minor GC,即对新生代进行GC。采用新生代GC收集器。如果发现Surivor不够,则原本在新生代的全部进入老年代,新对象进入新生代。
  • 大对象直接进入老年代。这是为了防止新生代之间发生大量的内存复制。
  • 长期存活的对象将进入老年代。如果新生代对象经历过一次minor gc,那age就+1,当age=15的时候,自动晋升到老年代。
  • 动态年龄判定。并不是必须达到MaxTenuringThreshold才能晋升到老年代,如果Survivor空间中所有对象大小大于Survivor空间的一般,年龄大于等于该年龄的对象可以直接进入老年代。其实可以理解,相当于在surivivor中,发现有一半的年龄相仿,说明这一半的几乎是同时建立的,可以看成一个大对象。
  • 空间担保分配。发生minor GC之前会判断老年代大小是否大于新生代总空间,如果大于,稳得一笔。如果小于,查看历次晋升的平均大小,如果老年代大于平均大小,就minor GC,否则Full GC。Full GC指的是发生在老年代的GC.

4. 虚拟机执行子系统

4.1. 类文件结构

  • class文件格式中包含两种数据类型
    • 无符号数字
  • 魔数CAFEBABE与版本号
  • 常量池
    • 字面量(语言层面)
      • 文本字符串,声明为final的常量值
    • 编译原理层面
      • 类和接口的全类名
      • 字段的名称和描述符
      • 方法的名称和描述符
    • 为什么要存这些?因为JAVA是虚拟机加载Class文件时动态连接的。也就可以理解为,虚拟机拿到常量池中的类和接口的全类名,然后去找到其实际的物理地址。这是类加载的内容。
  • 访问标志
    • 标示用于识别一些类或者接口层次的访问信息。
    • 是class还是接口
    • 是否是public
    • 是否是abstract
    • 是否是final
  • 类索引、父类索引和接口索引集合
    • 类索引要到常量池里去找符号引用
  • 字段表集合
    • 类变量
    • 实例变量
    • 不包含在方法中的局部变量
    • 修饰符(public,static,final,transient)通过标志位表示,字段名称和数据类型通过引用常量池中常量表示。
    • 不会列出从父类继承的字段
  • 方法表集合
    • 记录方法的修饰符,名称索引,描述符索引
    • 方法表集合中的代码,经过编译器编译成字节码指令后,放在名为code的属性中,code在属性表里
    • 如果不重写(override,继承的概念),就不会出现来自父类的方法 - 重载,即同名函数。在Java语言层面,返回值不能分辨重载函数,但是class文件结构可以。所谓的判断方法叫做特征签名。
  • 属性表集合
    • 属性表中的code保存代码,而其他部分储存元数据。
    • this指针的底层实现是编译器编译的时候把对this关键字的访问转化成了对普通方法参数的访问,然后在虚拟机调用实例方法的时候自动传入此参数。因此在实例方法的局部变量表中至少存在一个指向当前对象实例的局部变量。即this是一个默认指向当前实例的局部变量,无需申明,自动存在。
    • ConstantValue
      • 类变量的初始化有两种方式1.中初始化2.ConstantValue中,但是只能是基本变量和String,并且是final static。
    • Signature
      • java中泛型是擦除泛型,这样反射的时候就无法获得泛型的类型了,因此通过这个属性进行存储。

4.2. 类加载机制

4.2.1. 概述

虚拟机把描述类的Class文件加载到内存,并对数据进行校验,转化解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。java中,类型的加载,连接和初始化都在程序运行期间完成。

类从被加载到虚拟机内存开始,到卸载出内存位置,他的生命周期包括:加载,验证,准备,解析,初始化,使用和卸载七个部分。其中验证,准备,解析3个部分叫做连接。

JVM复习笔记

只有加载,验证,准备,初始化和卸载的顺序是确定的。

4.2.2. 类的加载过程

  • 何时开始加载?

    1. 遇到new,getstatic,putstatic,invokestatic这四条指令码的时候。即new一个对象,或者对类的静态字段有操作的时候(final除外,已经在常量池里放好了),或者调用静态方法。
      • 总结:常规的需要第一次使用实例或者类的时候。
    2. 反射。
      • 总结:动态的创建实例或使用类的时候。
    3. 初始化子类但父类还未初始化。因为子类会继承父类的类变量,不初始化这部分就没了。实例也一样,会继承实例方法,不初始化就没了。
      • 总结:儿子不能没有父亲啊。
    4. 主类会被先初始化。
    5. 通过java.lang.invoke.methodhandle,这是类似使用函数指针来调用函数的类。如果被调用的函数是static的,那么也必须加载类。
    6. 接口的不同之处
      • 在第3点中,子接口初始化时不会初始化所有的父接口,只会在真正使用的时候在初始化。也很好理解,因为接口可有无数个,全部初始化也太慢了。
  • 具体的类加载过程

    1. 加载
    • 通过类的全限定名来获得定义此类的二进制字节流。

    • 从流中所代表的的静态存储结构转化为方法区的运行时数据结构

    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区(1.7后在物理内存的元空间中)的类的各种数据信息的访问入口。

      • 数组

        • 数组类本身(字节码中以[权限类名形式存储的一个对象)由虚拟机直接创建,但是数组内的元素通过类加载器创建。
        • 数组内组件类型的(一维数组是元素,二维数组是组件类型的数组)是是引用,那就用正常的类加载过程去加载这个元素。
        • 数组内的组件类型如果不是引用,比如是int[],或者A[],虚拟机会把数组标记为与引导类加载器(启动类加载器,一种预定义的基本加载器)关联。
        • 数组类的可见性与他的组件类型一直。如果组件类型不是引用类型,则默认public。
      • java.lang,Class对象,存放在方法区中(元空间内),而不再堆中。

    1. 验证
    • 文件格式验证,即看看字节流是否符合class文件格式规范(以下均在内存的方法区中进行)
    • 元数据验证,看看元数据中是否有毛病
    • 字节码验证
    • 符号应用验证
    1. 准备
    • 准备阶段是真是为类变量,即static变量分配内存并设置类变量初始值的节点,这些变量所使用的内存都在方法区中进行分配。
    • 初始化是初始化为0,比如static int a = 123;在这个阶段初始化为0,在初始化节点中,才将a初始化为123
    • 如果是final static int a则会放置在ConstantValue中,会在准备阶段赋值。回归一下,ConstantValue必须是static final 基本+String类型的。
    1. 解析
    • 解析阶段是虚拟将将常量池中的符号引用替换成直接引用的过程。
      • 符号引用:以字符串的形式定位引用目标,与物理内存无直接关系。
      • 直接引用:以指针或句柄的形式直接指向物理内存中的地址。
    • 虽然多次解析请求很正常,但是还是会对解析结果进行缓存以避免多次解析。
    • 4.1 类或接口解析,假设当前代码的类为D,要把未解析过的符号引用N解析成直接引用C
      • 全限定名传递给D的类加载器去加载C
      • 如果是数组,先加载数组元素,再加载数组
      • 解析完成之前判断访问权限。
    • 4.2 字段解析
      • 按继承/实现关系从下向上寻找字段匹配。
      • 进行权限验证
      • 如果接口和父亲都存在同名static字段,会出现二义性问题。
    • 4.3 类方法解析
      • 和字段类似,向上匹配。
    • 4.4 接口方法解析
      • 不存在权限问题。
    1. 初始化
    • 从初始化阶段开始,才真正执行java代码。初始化阶段是执行类构造器()的过程。
      • 由所有类变量的幅值语句+近代语句块(static{})构成,顺序只和语句顺序有关,因此,静态语句块中只能访问到定义在静态语句块之前的变量。
      • 保证父类构造器在子类构造器之前执行完毕。
      • 上一条保证了子类一定在父类全部类变量初始化之后才初始化。
      • 不是必须的,如果没有类变量则没有cinit。
      • 如**何时开始加载?**中所述,接口中不能静态代码块,但是可以类变量赋值。但是接口在cinit的时候不会初始化所有父接口,除非用到了父接口的变量。
      • 虚拟机保证clinit是线程安全的。当多个线程要初始化同一个类的时候,会正确的加锁与同步。当一个线程在初始化的时候,另一个想要执行clinit的线程被阻塞,当被唤醒后,因为一个类只会执行一次clint,因此会放弃执行clinit方法。
  • 类加载器

    • 类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”过程放到java虚拟机外部去实现,实现这个的代码模块叫做“类加载器”。
    • 类加载器和类是一个二元组,这个二元组唯一确定一个类。即,必须要是同一个类加载器加载的同一个类,才被认定是同一个类。如果是不同类加载器加载的同一个类,也被判定为不同的类。
      • 为什么要这么做呢?加入你自定义了一个类加载器,强制不经过父类加载器直接加载自己写的java.lang.String类,此时你加载的这个String类和通过启动类加载器加载的不是同一个String类,因此后期在使用的时候,其效果不等同于我们日常用的String。
    • 双亲委派模型
      • 先导概念

        • 启动类加载器,HotSpot中使用C++实现,是虚拟机的一部分。用于加载\lib下的类,如rt.jar,其中包括了java.lang,java.sql等常见包
        • 所有其他类的类加载器,由java实现,独立于虚拟机外部,并且全部继承与java.lang.ClassLoader
          • 扩展类加载器 用于加载\lib\ext中的类
          • 应用程序类加载器 用于加载用户类路径上指定的类,如果用户没有自定义自己的类加载器,这个就是默认的类加载器
      • 双亲委派模型

        • JVM复习笔记
        • JVM复习笔记
        • 双亲委派机制指的是,除了最*的启动类加载器以外,所有的类加载器都要有父类加载器(这个父类不是通过继承实现,而是通过组合实现,比如当前类加载器是A,A中有一个parent对象代表A的父类加载器)。当一个类加载器收到类加载请求时,它会报告自己的父亲,然后父亲报告父亲,直到父亲无法加载这个类为止。
        • 好处是什么呢?假如你自己写了一个java.lang.Object,放在了classpath下,这个类被应用程序类加载器加载,但是你想加载的是lib下的那个object,此时就会发生冲突:你发现你的类并不是派生自Object的,这不符合java的基本特性。但是如果遵守了双亲委派机制,即使你自己写了一个java.lang.Object,类加载器在根据全类名去加载的时候,依然会找到lib下的java.lang.Object,而不是classpath下的。
        • 举一个例子说明如果不重视双亲委派机制出现的问题。你在lib\etc下放一个jar包,版本是a,在classpath下放一个jar包,版本是b。你想用的是b版本,但是因为采用双亲委派机制,永远加载的是lib\etc下的版本为a的jar包,就可能出现问题。
        • 实现思路:1.检查类是否已经被加载。2.如果没有,交给父亲加载。3.如果父类为空,说明自己是启动类加载器,那么直接交给启动类加载器加载。4.如果父亲无法加载,那么自己加载。
        • 破坏双亲委派模型
          • 第一次是最开始没有双亲委派模型,因此大家自定义classloader的时候都是重写loadclass方法,但是loadclass方法本身实现了双亲委派机制,因此为避免自己写loadclass覆盖了父类的loadclass,因此提供新方法findclass代替。这样用户只要重写findclass即可,不必重写loadclass,以保证类加载器必然是双亲委派机制的。
          • 重要第二次是类似JDBC这种,采用启动类加载器加载JDBC的类,但是JDBC里面的类需要调用到具体的某一类数据库驱动,这个驱动是在classpath下的,应该用应用类加载器加载,但是!当前代码在JDBC的类中,发现数据库驱动.jar还未加载,会通过当前的类加载器加载,即启动类加载器来加载数据库驱动.jar。此时,就出现了问题,本来应该用应用类加载器加载数据库驱动.jar,现在你非要用启动类加载器,而且还不能向下委派(假如是应用类加载器中用到了Object,会在应用类中执行双亲委派,直到找到启动类加载器加载Object),这不是崩了?所以需要启动类加载器调用应用类加载器来加载数据库驱动.jar。由此出现的技术叫做线程上下文加载器, 作用是为线程制定一个加载器。SPI(Service Provider Interface,服务提供接口)就采用了这种方式,在代码里新建一个线程用于指定加载器为应用类加载器,这样只要加载JDBC类即可。
          • 第三次,OSGI等为了实现热部署,代码热替换的程序动态性而破坏双亲委派机制。(根本原因是构建一个全新的委派机制以适应业务需求,隔离
            • 热替换:每当要替换的时候,新建一个classloader,然后加载新的class文件即可,重写findclass方法。
            • 以tomcat为例,为什么要重新设计类加载器?(Tomcat是基本符合双亲委派机制的,但是web classloader不双亲委托,
              -JVM复习笔记
              • 需求1:tomcat中部署两个不同的应用,不同应用可以使用不同版本jar包
                • 默认加载器:做不到,因为只识别权限类名。所以设计了WebAppClassLoader,各个webapp之间是隔离的,只对当前webapp可见。而且WebAppClassLoader是违反双亲委派的,有且仅有Java SE的包才双亲委派,第三方包拒绝双亲委派,如果加载失败,利用双亲委派机制再重新家在一次。

                https://blog.csdn.net/iteye_18979/article/details/82604271

              • 需求2:不同应用之间可以共享同种版本jar包
                • 可以保证唯一性。设计了一个shared classloader,不同的webapp之间可以可见且共享,但是和tomcat容器不可见。
              • 需求3:tomcat自己作为容器依赖的jar包和不同应用之间隔离
                • 只识别全限定类名不够。所以设计了一个catalinaLoader加载器,对其他全部隔离
              • 需求4:jsp会生成class文件,要实现热部署
                • 一个jsp对应一个加载器,jsp替换加载器也替换。所以设计了一个个JSPerLoader,jsp的加载器根本不需要进行双亲委派。
              • 备注:所谓隔离就是A ClassLoader去加载A路径下的,B加载B路径下的,互不连通,A的class,B ClassLoader无法访问。所谓可见是,我目录下的class你可以访问。可见与否是根据类加载器覆盖的路径来的,和是否双亲委派模型没有关系。
              • 顺序
                • Bootstrap classes of your JVM(rt.jar)
                • System class loader classes(bootstrap.jar、tomcat-juli.jar、commons-deamon.jar)
                • /WEB-INF/classes of your web application
                • /WEB-INF/lib/*.jar of your web application
                • Common class loader classes (在$CATALINA_HOME/lib里的jar包)
                • src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在 WEB-INF/class 中。而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。因此肯定是 java文件或者JSP文件编译出的class优先加载。

4.3. 虚拟机字节码执行引擎

4.3.1. 运行时栈帧结构

一个栈帧中包括了局部变量表,操作数栈,动态连接,方法返回地址和一些辅助的附加信息。一个栈帧对应一个方法,并且在编译之后,栈帧的大小以及完全确定了

JVM复习笔记

  • 局部变量表
    • 以slot槽为单位,一个slot存放32位的数据类型,double和long用两个slot存放。

    • 虚拟机通过索引的方法使用局部变量变,类似于之前的卡表思路。

    • 实例方法中,第0个索引的内容是this引用,用于传递方法所属实例对象的引用。

    • 为了节约栈帧空间,slot是可以重用的。即方法内可用代码块来重用slot。如下面的例子,如果没有int i=0,则slot没有被复用,导致GC不会回收64kb堆内存。

        public static void main(String[] args) {
            {
                byte[] placeholder = new byte[1024 * 1024 * 64];
            }
            int i = 0;
            System.gc();
        }
      
    • 局部变量没有默认值

  • 操作数栈。
    • 类似于剑指offer的习题,操作数入栈,遇到运算符弹出运算。
  • 动态连接
    • 每个栈帧包含一个指向运行时常量池中该栈帧所属方法的引用。符号引用一部分会在类加载时转化为直接引用,这个叫做静态解析,一种会在运行期间转化为直接引用,这种叫做动态链接。
  • 方法返回地址
    • 正常完整出口或者异常完成出口。

4.3.2. 方法调用

方法调用不等于执行方法,其核心在于执行哪一个方法。编译原理中的连接是指将编译的结果连接成可执行改代码,本质上是确定各部分的地址,将编译结果中的符号地址转化为实际物理地址。但是java的class文件不存在连接,即全部保存为符号引用,当类加载的时候,甚至是运行期间才能确定符号引用所对应的目标方法的直接引用,这也直接导致了C++和Java实现虚函数的不同之处。

  • 什么类型的方法是类加载时可以解析的?
    • 静态方法,私有方法,实例构造器,父类方法,final方法,统称为非虚方法。
  • 分派
    • 静态分派。即重载(就是同各类多个方法名相同,根据参数类型及个数分别),参数由静态类型(即指针,引用,就是A a = new A()中的a)确定。
    • 动态分派。即重写(父子之间的关系,多态),是根据动态类型,即堆内存中的对象确定。
    • 单分派与多分派。java是静态多分派,动态单分派,即重载时根据调用方法者(即静态类型的指针,引用)+方法参数确定,重写的时候只根据堆内存中的对象确定。
  • 如何实现重写?
    • invoke virtual
      • ![https://img-blog.csdn.net/20160330150306885]
      • 从上图可以很清晰的了解到,girl和boy都重写了toString,eat,speak方法,因此他们在方法表汇总各自享有指针。
      • 父类子类享用完全相同格式的两张方法表,如果子类重写了方法A,则替换掉子类方法表中A方法地址。
      • JVM复习笔记
      • 父类引用无法调用子类特有方法,调用子类重写方法时会根据动态类型来实现多态。本质在于是根据静态引用查找方法表,然后根据动态类型来确认(其实是根据this,但是this就是指向堆内存中的那个对象)是否要更换成子表。根据上一条,若重写,子表的对应位置已经换成了重写后的方法,因此实现动态调用。
    • 如果不是继承父类而是实现借口呢?
    • invoke interface
      • 接口无法调用接口中不存在的方法。
      • 因为接口可以多继承,因此无法使用基于偏移量的方法表查找思路,而是用搜索的思路。

4.4. 编译期优化

  • 3类编译器
    • 前端编译器,把.java变为.class,比如javac
    • JIT编译器,Just In Time Compiler,解释的同时将一些经常用到的热点代码进行编译
    • AOT编译器,把.java直接编译到本地机器码,类似C++的编译器
  • 性能优化放在运行期,编译期即javac只做语言优化,即语法糖。因为运行期是针对.class的,运行期优化即JIT优化可以对所有字节码文件进行优化,而不只是java语言。
  • 语法糖
    • 泛型与类型擦除,属性表的code中擦除泛型,但是元数据中保留了泛型的信息,因此可以通过反射获得。
      • class文件运行返回值不同的同名同参数方法存在,java语言不允许。
    • 自动装箱,拆箱,遍历循环
    • 条件编译,使用判断条件为常量的if语句,就不会编译else以后的内容

4.5. 运行时优化

  • 解释器与编译器并存的架构。
    • 解释器启动快,不需要编译时间。
    • 对热点代码进行编译,提高执行效率。
    • 若编译器优化过于激进,可退回解释器执行。
  • Client与Server是两个JIT编译器,可以指定,但是虚拟机会根据主机性能自动选择。
    • client有更快的编译速度,server有更好的编译质量。
    • client相对轻量,server编译的相对彻底。
    • client新生代采用serial,老年代serial old,server新生代parallel scanvenge,老年代serial old
  • 编译对象与触发条件,即何时调用编译器?编译哪些代码?
    • 编译哪些代码?
      • 多次被调用的方法。
      • 多次执行的循环体。JIT依然是将整个方法替换,此时方法还在栈上,因此被称为栈上替换。
    • 何时调用编译器?
      • 方案1 定时检测栈顶,出现越频繁的就说明约热点,好处是简单高效,缺点是如果线程阻塞,热点代码就会出错。
      • 方案2 每个方法中都建立计数器,每次执行就++,超过某一阈值就编译。

4.6. JVM性能调优

4.6.1. JDK监控和故障处理工具

  • jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
    • 我写的那个markdown的图片小工具偶尔会出问题,就可以用jps确定pid之后去任务管理器关掉重启。
    • -l 输出主类的全名,如果是jar包输出包路径 JVM复习笔记
    • -v 输出虚拟机进程启动时的jvm参数 比如-Xms8m JVM复习笔记
  • jstat JVM Statistics Monitoring Tool 用于收集HotSpot虚拟机各方面的运行数据,监视虚拟结构中运行状态信息
    • 可以显示类装载,内存,垃圾收集,JIT编译等运行数据
    • -class 类装载,卸载等数量及时间 JVM复习笔记
    • -gc/gcxxxx 监视堆状态 JVM复习笔记
    • -compiler 数据JIT编译器编译过的方法,耗时等信息 JVM复习笔记
    • -printcompilation 输出已经被编译器编译的方法 JVM复习笔记
  • jinfo Configuration Info for java 显示虚拟机配置信息
    • -flag 可以查询某个配置的信息 JVM复习笔记
  • jmap Memory Map for Java 生成java虚拟机的内存转储快照,heapdump。用于打印指定java进程的共享对象内存映射或堆内存细节。
    • -dump 生成heapdump,JVM复习笔记
    • -heap 可以查看当前java堆内存详细信息 JVM复习笔记
      • 由此也可以看出堆中包含字符串常量区。其中newSize表示新生代初始大小,newRatio=2表示新生代占比1,老年代占比2,即老年代占2/3.survivorRatio=8表示Eden占8,Survivor占2,即8:1:1,如果=12就是12:1:1。CompressedClassSpaceSize表示64位指针压缩到32位,降低内存占用。
    • -histo 显示堆中对象统计信息,包括类,实例数量,合计数量 JVM复习笔记
  • jhat JVM Heap Dump Browser,用于分析heapdump文件,运行后会给出一个端口,访问端口则可以查看heapdump
    • 一般不建议采用jhat,因为比较简陋。
  • jstack stack trace for java,显示虚拟机的线程快照,线程快照即每一条线程正在执行的方法堆栈的集合
    • 主要目的是定位线程出现长时间停顿的原因,如线程间死锁,死循环,请求外部资源(IO、Socket的accpet之类的阻塞)导致的长时间等待。JVM复习笔记

4.6.2. 调优案例实战分析

  • 硬件提高后导致堆内存分配很大,比如12GB,进行一次full GC时间过长如何解决?
    • 单机模式,但是程序员代码要有控制Full GC的能力。一旦出问题,heapdump将非常大,只能采用jstat来查看一段时间从而分析问题。
    • 单机建立多虚拟机的逻辑集群,通过负载均衡服务器分配。这样full gc时间短,如果对CPU资源不敏感可以采用CMS。
  • 集群间同步导致的内存溢出如何解决?
    • 先用heapdump或heapdumpOnOOM检查是否存在大量的单一对象,如果有,检查其为何出现,并通过代码控制。
    • 集群同步问题首先考虑网络重发是否导致内存泄漏,然后考虑是否读写过于频繁导致内存溢出。
  • 堆外内存导致的溢出问题?
    • 比如你给堆内存分配了很大的堆,但是还是出OOM问题,这时候考虑是不是直接内存溢出,1.8之后考虑是不是元空间占用过多。
    • 除了java堆和永久带,以下区域也会占用较多内存
      • 直接内存,即物理机内存,OOM错误。
      • 线程堆栈。分配线程要使用堆内存,不够分配线程就会报OOM(每个线程有默认分配的内存大小,jdk1.5后是1M,这部分消耗的是直接内存,而不是JVM的堆内存),或者栈扩展时无法申请到足够的内存也会报OOM。如果是栈空间不够,线程的栈帧不够用了,会报栈溢出。
      • JNI代码、本地应用软件也要占用内存
      • 虚拟机和GC也要占用内存
  • 外部命令导致系统缓慢
    • 即采用了java中一些重量级的命令导致CPU占用率高,可采用其他方式
  • 大量数据分配导致大型对象很多
    • 使用合理的数据结构降低无用数据
    • 取消新生代,让新生代直接进入老年代
  • 实用指令
    • 堆设置
      • Xms:初始堆大小
      • Xmx:最大堆大小
      • Xmn 设置年轻带大小
      • Xss 设置每个线程的堆栈大小,默认为1M
      • XX:NewSize=n:设置年轻代大小
      • XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
      • XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
      • XX:MaxPermSize=n:设置持久代大小
    • 收集器设置
      • XX:+UseSerialGC:设置串行收集器
      • XX:+UseParallelGC:设置并行收集器
      • XX:+UseParalledlOldGC:设置并行年老代收集器
      • XX:+UseConcMarkSweepGC:设置并发收集器
    • 垃圾回收统计信息
      • XX:+PrintGC
      • XX:+PrintGCDetails
      • XX:+PrintGCTimeStamps
      • Xloggc:filename
    • 并行收集器设置
      • XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
      • XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
      • XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    • 并发收集器设置
      • XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
      • XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

4.7. 牛客网上的问题

JVM内存管理机制,GC算法

解释CMS

类加载机制 一个.class从加载到运行经历了那些步骤

担保机制 minorGc full gc

运行时数据区域

jvm调优(xnn,xss,xmn,xms讲了一同)

fullgc应该从哪里找(我说jmap dump下来用map分析,接着问,如果不让dump呢,我说jstat分析一段时间的)

JVM GC,对象创建,多态,扯了分派

JVM有哪些部分构成?

jvm分代回收策略

JVM调优实践,JVM分区,栈堆空间,分配策略

JVM内存模型?如果给一个类,里面只有一个main方法,方法里面只有一句System.out.println(“helloworld”),问运行这个类会在Java内存模型里发生什么? “helloworld”存储在哪里?

ClassLoder与JVM里的源码实现

连续的String拼接JVM是如何完成的(回答在编译器被优化为StringBuild再进行append())

jsatck,jstat以及JVM的相关参数和JVM日志的查看

jvm内存模型,1.6 1.7 1.8哪里不同

jvm调优,命令行工具 jstack jmap

说几个垃圾回收器,cms回收器有哪几个过程,停顿几次,会不会产生内存碎片。老年代产生内存碎片会有什么问题。问我有没有做过JVM优化

标记清除多次后老年代产生内存碎片,引起full gc,接下来可能发生什么问题

gc是否会有停顿或者延迟

Jvm的参数设置

Jvm的参数你知道的说一下

为什么java可以做到跨平台

对JVM了解多少?我当时提到了内存模型 ,他问我工作线程的内存如何释放

有没有用过JVM相关工具?

你知道哪些jvm的参数?它们什么时候要设置,有什么作用?

jvm多态原理。invoke static 等指令。符号因用户 直接引用。方法表中进行找出

介绍一下JVM,OOM

jvm中适合高吞吐量的垃圾回收器