深入理解Java虚拟机

第二章

虚拟机组成

虚拟机由五大部分组成

  • 程序计数器 线程私有
  • 本地方法栈 线程私有
  • 虚拟机栈 线程私有
  • 方法区 线程共享
  • 堆 线程共享
  1. 程序计数器可以看作为字节码指示器指示运行流程
  2. 本地方法栈和虚拟机栈功能差不多但是它为Native方法(非java方法)提供支持,在有些jvm虚拟机中把本地方法栈和虚拟机栈合为一个
  3. 虚拟机栈也就是常说的栈,但是又不等同,在里面一个方法对应一个栈桢,里面存放已知的类型int、float等,同时还有引用变量指向堆中的地址
  4. 堆主要存放对象的信息还有数组等
  5. 方法区有些又称为永久代这里主要存放常量,但并不是不回收的。主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等

对象的创建

  1. 虚拟机在遇到new指令时,首先检查指令的参数是否能在常量池中定位到一个类的附后引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过

  2. 在类加载检查通过后,虚拟机为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确认(Java对象元信息中存储)。分配内存有两种方法

    1. 假设Java堆中的内存是绝对规整的,所有用过的内存放在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把这个指针向空闲空间那边移动一段与对象大小相等的距离,这种分配称为指针碰撞
    2. 如果Java堆中并不是规整的,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录上那些内存块是可用的,在分配的时候从列表中找出一块足够大的空间划分给对象实例,并更新列表上的纪律,这中分配方式称为空闲列表

    具体选择哪种分配方式有Java堆是否规整决定,而Java堆是否规整又由对采用的垃圾收集器是否带有压缩整理的功能决定。

  3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)

  4. 接下来虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如果才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息

  5. 上面工作完成后,从虚拟机的角度来看,新的对象已经产生了,但是从Java程序角度来看,对象创建才刚刚开始方法还没有执行。

对象内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充

对象访问定位

Java通过栈上的reference数据来操作堆上的具体对象,由于reference类型在Java虚拟机中只规范了一个指向对象的引用,所以对象访问方法取决于虚拟机实现而定。目前主要有两种使用句柄直接指针

  1. 使用句柄:在Java堆中分配一块作为句柄池reference中存储的是对象句柄地址,句柄中有对象实例数据指针指向堆中实例池中的对象实例数据,对象类型数据指针指向方法区中的对象类型数据
  2. 直接指针:reference直接指向队中的对象实例数据,实例数据中包含对象类型数据的指针

虚拟机栈和本地方法栈溢出

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

第三章

判断对象是否回收

  • 引用计数算法
    在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器值就减一,引用计数法简单效率高,但是存在相互引用问题,例如两个对象相互引用就造成无法回收。
  • 可达性分析算法
    目前判断对象是否存活的主流算法,使用一系列的"GC Roots"对象作为起点,从这些结点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。
    可作为GC Roots的对象有
    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI引用的对象

引用分类

JDK1.2之后,将引用分为强引用,软引用,弱引用,虚引用

  • 强引用就是指在程序代码中普遍存在的,类似"Object obj = new Object()"这类引用,只要强引用还存在,GC永远不会回收掉引用的对象。
  • 软引用是用来描述一些还有用但非必须的对象。对于软引用,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之内进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
  • 弱引用也是用来描述非必须对象的,但是他的强度比软引用更弱一些,软引用关联的对象只能生存道下一次GC之前。
  • 虚引用也成为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个实例对象。为一个对象设置虚引用的唯一目的就是能在这个对象被GC回收时受到一个系统通知。

生存还是死亡

即使在可达性分析算法中还是不可达的对象,也并非"非死不可"。要宣告一个对象的死亡,至少需要两次标记过程:如果一个对象没有与GC Roots的引用链则会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖此方法时,或此方法已经被虚拟机调用过,则虚拟机都视为没有必要执行。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将被放置到F-Queue的队列中,并在稍后由一个由虚拟机创建的、低优先级的Finaizer线程中去在执行它,这里的执行是指虚拟机会执行这个方法,但不会承诺等待它执行完,原因是如果执行缓慢或这死循环,将会导致F-Queue队列其他对象一直处于等待状态,甚至导致整个GC崩溃,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果在finalize()中此对象与引用链中的任意一个对象建立关系则会被移除即将回收的集合。如果没有则回收。
注意finalize()方法只会执行一次。

回收方法区

Java虚拟机规范中说过可以不要求虚拟机在方法区实现GC,而且在方法区回收性价比比较低。
永久代GC主要分为:废弃的变量与无用的类

  • 废弃的变量:如果字符串“abc”进入了常量池,但当前系统没有任何一个String对象叫做“abc”的,就是没有任何String对象引用常量池中的“abc”,则为废弃变量
  • 无用的类,比较复杂要满足3个条件,而且并不是必然回收
    • 该类所对应的所有实例都已被回收
    • 加载该类的ClassLoad已被回收
    • 该类对应的java.lang.Class对象没有任何地方被引用,无法再任何地方通过反射访问此类

垃圾回收算法

  • 标记-清除算法
    分两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
    不足两个问题:效率问题,标记和清除两个过程的效率都不高;空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会造成需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次GC。
  • 复制算法
    将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,然后把已使用的那一块全部清理掉。
    这样内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行搞笑,代价是将内存缩小为原来的一半。
    这种算法在新生代里使用。
  • 标记-整理算法
    在老年代中使用此算法,此算法的标记过程与标记清除一样,但是后续步骤是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存

分代收集算法

当前商业虚拟机都采用“分代收集”,根据对象的存活周期不同将内存分为几块,一般是分为新生代和老年代,在新生代中每次都是大量的对象被回收所以采用复制算法,在老年代中存活率高没有额外的空间做担保所以采用“标记-清除”或者“标记整理”

回收类型

  • 新生代GC(Minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对)。Full GC会比Minor GC慢10倍左右。

内存分配与收回机制

对象的内存分配,往大方向讲,就是在堆上分配,对象只要分配在Eden区上,如果启动了本地线程分配缓冲,将按照线程的优先在TLAB上分配。少数情况下也可能会直接分配在老年代中。

对象优先在Eden分配

大多数情况下,对象在Eden区分配。当没有足够空间分配时,虚拟机会进行Minor GC

大对象直接进入老年代

所谓的大对象指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串和数组。

长期存活的对象将进入老年代

为了区分哪些对象应该放在新生代,哪些对象需要放到老年代,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对于在Survivor区中每熬过一次Minor GC,年龄就增加1,当它年龄增加到一定程度(默认15岁),就会晋升到老年代中。

动态对象年龄判定

为了能更好的适应不同程序的内存情况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivo空间中相对年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

第七章

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
什么情况下进行第一阶段:加载,Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把我。但是对于初始化阶段Java规定了只有5种情况必须立即进行初始化。

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,必须进行初始化。生成这4条指令最常见的Java代码场景是:使用new关键字、读取或设置一个类的静态字段,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持是,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_INVOKEsTATIC的方法句柄,并且这个方法句柄所对应的类没有进行初始化。
原文地址:https://www.cnblogs.com/ingxx/p/11275175.html