Fork me on github

深入理解java虚拟机

深入理解java虚拟机读书笔记

1 java内存区域与内存溢出异常

1.1 java虚拟机运行java程序回把它所管理的内存划分为若干个不同数据区域。

  • 程序计数器是一款较小的内存空间,可以看做当前线程所执行字节码的行号指示器,为了线程互不影响,每个线程都有一个独立的程序计数器。
  • 虚拟机栈,也是线程私有,生命周期与线程相同,每个方法被执行的同时都会创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等信息。
  • 本地方法栈 与虚拟机栈发挥的作用极为相似,区别是前者前者为执行java方法(也就是字节码服务),而本地方法栈则是为虚拟机使用到本地(Native)方法服务。
  • java堆 对于java应用程序来说java堆是虚拟机管理内存当中最大的一块,java堆是被所有线程共享的一块内存区域,此区域唯一的目的就是存放对象实例,所有对象以及数组都应当在堆上分配。
  • 方法区 与java堆一样是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
  • 运行时常量池 它是方法区等一部分,Class文件中除了有类的版本、字段、方法、接口等描述性信息外,还存在常量池表,它用于存放编译期间生成的各种字面量与符号引用,这一部分将在类加载后存放到方法区的运行时常量池当中。
  • 直接内存 它并不是虚拟机运行时数据区的一部分,也不是《java虚拟机规范》中定义的内存区域,但是这部分内存也被频繁的使用。

 

2 HotSpot对象探秘

2.1 对象的创建

过程:当java虚拟机遇到一条字节码new指令的时候,首先查找这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个类是否已经被加载、解析与初始化、如果没有则执行内加载过程。

2.2 对象分配内存

对象所需要的大小在类加载之后便可以完全确定,为对象分配空间的任务实际上等同于在java堆中划分一块等大堆内存。

  • 指针碰撞 如果内存空间是规整的,采用此方法从堆中连续分块的分配内存。
  • 空闲列表 内存不规整,虚拟机就必须维护一个列表,记录哪些内存可用,再按照对象的大小根据列表的大小分配。

2.2 对象的内存布局

在Hotspot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分,对象头、实例数据、对其填充。

  • 对象头部分包含2类信息(1)用于存储对象自身的运行时数据、如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,(2)其他部分则是类型指针,即对象指向它的类型元数据的指针、java虚拟机通过这个指针来确定该对象是哪个类的实例。
  • 实例数据 该部分存储真正有效的信息,我们在程序代码里面定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类当中定义的字段都必须记录起来。
  • 对其填充 这部分并没有特别的含义,只是起到占位符的作用。

2.3 对象的访问定位

  

句柄技术,需要在java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,该句柄中包含对象实例数据与类型数据的给自信息

直接指针 此时java堆当中的内存布局就必须考虑如何放置访问类型数据的相关信息,reference当中存储的就是对象地址,如果只是访问对象本身的话就不需要多一次简介的访问开销。

在java虚拟机当中主要使用的是直接指针技术。

 2.4 常见的OutofmemoryError异常

  1. java堆溢出,不断创建对象,并且GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象。
  2. 虚拟机栈与本地方法栈溢出,这两个在虚拟机当中并不做更加细致的区分,其溢出导致的原因有2个 (1)线程请求的栈深度大于虚拟机所运训的最大深度,StackOvearflowError异常,(2)如果虚拟机栈允许动态扩展,当扩展容量无法申请到足够的内存空间时,OutOfMemoryError异常。
  3. 方法区和运行时常量池溢出

 

2 垃圾收集器与内存分配策略

2.1 概述

垃圾收集器进行垃圾收集需要完成以下三个步骤,1 哪些内存需要回收、2 什么时候回收、3 如何回收

2.2 对象已死

进行GC时需要判断堆里面哪些对象活着,哪些对象已经死亡。

2.2.1 引用计数法

规则:在对象当中添加一个引用计数器,每当有一个地方引用它,计数器数值加1,当引用失效的时候就减去1,当计数器数值为0,就可以对该对象进行GC。

问题:相互引用所带来的循环依赖问题。

2.2.2 可达性分析法

 

 

 规则:通过一系列称为“GC Roots”的根对象作为起始节点集合,根据引用关系不断向下面搜索,搜索走过的路径称之为“引用链”,如果某个对象到GC Root之间没有任何的引用链相连,或者用图论的话来说从GC到这个对象不可达的时候,则证明此对象不可能再次被使用。

2.2.3 再谈引用

在JDK1.2版本之后,java对引用的概念进行了相关扩充,可以分为,强引用,软引用,弱引用与虚引用。

  • 强引用 是最传统“引用”的定义,无论任何情况下,只要强引用还存在,则垃圾收集器就永远不会回收被引用的对象。
  • 软引用 来描述一些还有用,但是非必需的对象,在系统将要发出内存溢出异常前,将会对这些引用对象进行二次回收。
  • 弱引用 也是用来描述非必需对象,强度比软引用更弱,被其引用的对象只能生存到下一次垃圾收集发生为止,无论内存是否足够,都会回收掉。
  • 虚引用 最弱的引用关系,其唯一目的是为了能在这个对象被收集器回收时收到一个系统通知。

2.2.4 生存还是死亡

在可达性分析中判定为不可达的对象,也不是“非死不可”的,这时候他们暂时还处于“缓刑”阶段,真正宣告一个对象死亡,至少需要经过2次标记过程,若在对对象进行可达性分析之后发现没有与GC Roots相连接的引用链,那将被第一次标记,随后进行一次筛选,条件是是否需要执行finalize()方法,假如没有覆盖finalize()方法,以及该方法已经被虚拟机调用过,那么就没必要回收该对象。

2.2.5 回收方法区

方法区的回收内容主要可以分为两部分,废弃的常量以及不再使用的类型,判断一个常量是否“废弃”还是相对简单,而要判断一个类型是否属于“不再被使用的类”的条件就比较苛刻了。它需要同时满足一下几个条件。

  1. 该类的所有实例已经被回收,即java堆当中不存在该类的任何派生子类的实例。
  2. 加载该类的类加载器已经被回收。
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法再任何地方通过反射访问该类的方法。

 

2.3 垃圾回收算法

2.2.1 分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,它建立在两个分代假说之上:

  1. 弱分代假说 绝大多数对象都是朝生夕灭的
  2. 强分代假说 熬过越多次GC过程的对象就越难以消亡。
  3. 跨代引用假说 跨代引用相对于同代引用来说仅占极少数
  • 标记清除法

原理:算法分为“标记”与“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成之后,统一回收所有被标记的对象。

缺点:1 执行效率不太稳定,假若有大量对象需要被清除,这时候就必须进行大量的标记和清除动作,导致执行效率降低。2 清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致在以后程序运行过程中需要分配较大对象而无法找到足够的连续内存而不得不进行另一次GC。

  • 标记复制法

 

 

原理:为了解决标记清除面对大量可回收对象时执行效率低的问题,它使用一种称为“半区复制”的垃圾收集方法,它可以将内存按照容量划分为大小相等的两块,每次只能使用其中的一块,如果内存使用完,就把依旧存活的对象复制到另一块上面,然后一次清除掉已经使用的内存空间。

缺点:将可用内存缩小到原来的一半,空间浪费未免太多。

  • 标记整理法

 

 

原理:为了解决标记复制算法在对象存活率较高的情况下面会进行较多的复制操作,效率将会降低,同时为了不浪费50%的空间,标记整理本质在于它是一种移动式的回收算法,它使得所有存活的对象都往内存的一端移动,然后直接清除掉边界以外的内存。

 

2.4 HotSpot的算法细节实现

根结点枚举,所有收集器在根结点枚举这一步骤就必须暂停用户线程的,毫无疑问,根结点枚举和之前整理内存碎片一样会面临“stop the world”的困扰,现在可达性分析耗时最长的查找引用链的过程已经可以做到与用户线程一起并发。

并发的可达性分析

为了降低用户线程的停顿,我们要理解为什么必需在一个能保障一致性的快照上进行对象图的遍历,在这里我们借用“三色标记”

  • 白色 表示尚未被垃圾收集器访问,在可达性分析开始的时候,所有的对象都是白色的,若在分析阶段结束的阶段,依然是白色的对象,代表不可达。
  • 黑色 表示对象已经被垃圾收集器访问过,且这个对象所有的引用都已经扫描过。
  • 灰色 表示该对象已经被垃圾收集器访问,但是该对象至少还存在一个引用还没有被扫描到。

 

 

wilson证明了,当且仅当以下两个条件同时满足的时候,会产生“对象消失的问题”,即原本应该是黑色的对象被误标成白色。

1 赋值器插入了一条或者多条从黑色对象到白色对象的新引用

2 赋值器删除了全部从灰色对象到白色对象的直接或者间接引用

增量更新 破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就讲这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系的黑色对象为根,重新扫描一次,可以简单的认为黑色对象一旦新插入指向白色对象的引用之后,它就会变成灰色对象了。

原始快照 破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系的时候,就将这个要删除的对象记录下来,然后在结束后,再以灰色对象为根,重新扫描,这也可以简化理解为,无论引用关系删除与否,都会按照刚开始那一刻的对象快照来进行搜索。 

 

2.5 经典的垃圾收集器

  • Serial收集器 他是最经典的,历史最悠久的收集器,但是它是一个“单线程”的收集器,每工作一段时间就会出现“stop the world”的情况,体验度不好。
  • ParNew收集器 实质上是Serial的多线程并行版本,它是不少运行在服务端模式下面的HotSpot虚拟机。
  • Parallel Scavenge 它是基于标记复制算法实现的收集器,它在诸多特性上面看来与ParNew类似,但是它的关注点有很大的不同之处,CMS等收集器关注的是尽可能缩短垃圾收集器收集的时候用户线程的停顿时间,而本收集器则是达到一个可以控制的吞吐量。
  • CMS 它是一种以获取最短回收停顿时间为目标的收集器。
  • Garbage First 该收集器是收集器技术发展史上面里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式,到了后期,G1提供了并发的类卸载支持,补全了其计划功能的最后一块拼图-“全功能的垃圾收集器”,且该收集器是的垃圾收集进入到Mixed GC模式。

 

3 虚拟机类加载机制

3.1 概述

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

3.2 类加载时机

一个类从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段。

 

 什么时候进行初始化?

  • 遇到new getstatic putstatic invokestatic这四条字节码指令的时候,如果类型没有进行初始化,就必须触发其初始化阶段。
  • 使用java.lang.reflect包的方法对类型进行反射调用的时候
  • 当类初始化时,发现其父类还没有进行初始化,就需要先触发其父类的初始化行为。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类
  • 当使用jdk7新加入的动态语言支持时,如果他提供的一个实例解析为曲柄,先初始化曲柄对应类的初始化

3.3 类加载的过程

1 加载是第一个阶段,在加载期间,虚拟机需要完成以下三件事情

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。

但是数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由java虚拟机直接在内存中动态构建出来的。

2 验证是链接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全,其验证的内容有,文件格式验证、元数据验证、字节码验证、符号引用验证

3 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上来讲,这些变量所使用的内存都应当在方法区中进行匹配。

4 解析阶段是java虚拟机将常量池内的符号引用替换为直接引用的过程。

--符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义的定位到目标即可。

--直接引用:直接引用是可以直接指向目标的指针、相对偏移量,或者是一个能够间接定位到目标的句柄。

解析动作主要针对,类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行。

5 初始化是类加载的最后一个步骤,之前介绍的几个类加载的动作里面,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部调参数以外,其他动作都完全由java虚拟机来主导控制,直到初始化阶段,java虚拟机才真正开始执行类中编写的java程序代码,将主导权转交给应用程序

3.4 类加载器

“通过一个类的全限定名来获取描述该类的二进制字节流”,这个动作放到java虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需的类,实现这个动作的代码被称为“类加载器”

1 类与类加载器

比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个java虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相同。

2 双亲委派机制(面试重点)

站在java虚拟机的角度来看,只存在两种不同的类加载器,一种是启动类加载器,另一种是其他所有类的加载器。

但是在java开发人员来看,类加载器应当划分的更加细致一些,自jdk1.2以来,java一直保持着三层类加载器,双亲委派类加载结构。其包含启动类加载器、扩展类加载器以及应用程序类加载器。

 

 

posted @ 2022-06-25 15:47  了不起的盖茨比  阅读(123)  评论(0)    收藏  举报
Copyright © 2021 LinCangHai
Powered by .NET 5.0 on Kubernetes