
JVM(HotSpot虚拟机)学习笔记
一、内存区域划分
- 运行时数据区域(JVM运行时的专用内存空间),可划分为如下2类
- 线程共享的区域
- 堆:存放字符串常量池、大部分对象实例
- 方法区(由永久代实现,jdk1.8已经移至本地内存区域,由元空间取代,减少内存溢出的可能)
- 线程私有的区域
- 虚拟机栈,本地方法栈(HotSpot将2者合为一个栈实现):用于执行方法
- 程序计数器:读取执行指令
- 线程共享的区域
- 本地内存(OS管理)
- 元空间:存放运行时常量池,存取类的相关消息等)
- 直接内存
二、对象
1. 对象创建的过程
- 类加载检查
- new对象时,判断常量池中是否能定位到这个类的符号引用,并检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 分配内存:从堆分配内存给对象
- 2种分配策略
- 指针碰撞(堆内存规整,不存在内存碎片时):可用内存与已使用内存的分界指针进行移动来分配
- 空闲列表(堆内存不规整):可用内存由一个列表维护,找一块足够大的内存块分配
- 内存分配并发安全解决策略
- 每一个线程预先在 Eden 区分配一块儿内存TLAB,分配对象内存优先在TLAB分配,不够则在堆内存使用CAS+失败重试策略分配
- 2种分配策略
- 初始化零值:初始化对象属性字段的默认值(不包括对象头)
- 设置对象头:把关于对象的设置(如对象哈希码,GC分代年龄等)的信息存放在对象头
- 执行init方法:即执行构造函数进行初始化
2. 对象的访问定位(reference如何找到目标对象)
- 句柄:从堆中划分一块内存作为句柄池,reference 中存储的就是对象的句柄地址,句柄中包含了对象实例数据(堆中的实例池)与对象类型数据(方法区)各自的具体地址信息。(优点,对象移动时,改变句柄的指针即可,无需修改reference)
- 直接指针:reference 中存储的直接就是对象的地址。(优点,直接访问,速度快)
三、垃圾回收(GC,Garbage Collected)
1. 堆空间基本结构
- 新生代内存(Young Generation):分为Eden,S0,S1(Survivor)
- 老生代(Old Generation)
- Metaspace(元空间,存在于本地内存,不再是存在于堆中的永久代(Permanent Generation),避免内存溢出问题)
2. 内存分配策略
- 对象一般先在 Eden 区分配,如果Eden空间不够,发起一次Minor GC,尝试放入Survivor区,还是不够则尝试放入老年代
- 大对象直接进入老年代,减少新生代的垃圾回收频率和成本。
- 长期存活的对象将进入老年代:每个对象设置一个年龄(Age)计数器。
- Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1。对象在 Survivor 中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中
- 空间分配担保:确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。(进行Minor GC前会检查,不满足则直接进行Full GC)
3. GC策略
- 部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC):收集整个 Java 堆和方法区
4. 死亡对象判断方法
- 引用计数法:每当有一个地方引用它,计数器就加 1;(无法解决循环引用问题)
- 可达性分析算法
- 思想:
- 设置一些“GC Roots” 的对象作为树起点,无法被这些点到达的对象标记为不可达,可以被回收。可回收对象要经历两次标记过程,如果一直没有与引用链上的对象建立关联,才会被真的回收。
- 可以作为 GC Roots的对象:(特征:当前一定不会死亡的对象)
- 虚拟机栈引用的对象(当前方法正在运行,局部变量引用的对象必须存活,比如 Integer a=b; a是局部变量,b是被a引用的对象)
- 本地方法栈(Native 方法)、JNI(Java Native Interface)引用的对象(非java实现的对象,JVM 无法回收,由实现的语言管理)
- 方法区中类静态属性引用、常量引用的对象(静态变量的生命周期与类绑定,除非类被卸载,否则静态变量引用的对象必须存活。)
- 所有被同步锁持有的对象
- 三色标记法(把访问对象按照”是否访问过”这个条件标记成三种颜色)
- 白色:尚未被扫描过的对象。若在分析结束的阶段,仍然是白色的对象,即代表不可达。
- 黑色:这个对象的所有引用都已经扫描过,本次GC无需再扫描此黑色对象(黑色对象不可能直接(不经过灰色对象)指向某个白色对象)
- 灰色:该对象至少有一个引用未必扫描过(未扫描完)
- 并发标记存在的问题
- 浮动垃圾:并发标记为黑色的对象之后取消全部引用,但是黑色结点此次GC不会被重新标记扫描,垃圾只能下阶段清除
- 对象消失:标记时一个节点的引用被取消,但是后面又有其他黑色结点引用它,可是黑色结点不会再被扫描,因此认为当前结点没有引用,为垃圾,错误清除对象
- 对象消失问题的解决
- 破坏2个条件之一(利用读写屏障实现,类似AOP切面)
- 赋值器插入了一条或者多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
- 增量更新法(CMS,写后屏障:记录所有新增的引用关系):破坏第一个条件,将黑色指向白色的新引用记录下来,并发扫描结束再以这些黑色结点为根扫描,确保正确标记
- 原始快照法(G1,写前屏障:记录被删除的旧引用关系):破坏第二个条件,记录灰色到白色的删除操作,并发扫描结束再以这些灰色结点为根扫描旧引用的图,即删除对象成为浮动垃圾,下次GC才会删除
- 读屏障(ZGC):当用户线程读取对象引用时,若为白色标记则触发读屏障标记为灰色
- 破坏2个条件之一(利用读写屏障实现,类似AOP切面)
- 思想:
5. 垃圾收集算法
- 标记-清除算法
- 分为“标记(Mark)”和“清除(Sweep)”阶段:先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
- 缺点:1,标记和清除两个过程效率都不高。2,标记清除后会产生大量不连续的内存碎片。
- 标记-复制算法
- 将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
- 缺点:1,可用内存缩小为原来的一半。2,如果存活对象数量比较大,复制性能会变得很差,不适合大内存的老年代
- 标记-整理算法
- 先标记出所有不需要回收的对象,把所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 整理效率不高,适合老年代这种垃圾回收频率不是很高的场景。
- 分代收集算法(主流)
- 新生代选择“复制”算法,只需要复制少量对象就可以完成GC。(因为每次收集都会有大量对象死去)
- 老年代选择“标记-清除”或“标记-整理”算法进行GC。因为其对象存活几率是比较高的,而且没有额外的空间对它进行分配担保
6. 垃圾收集器
Serial(串行)收集器
- 单线程,且在进行GC时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
- 新生代采用标记-复制算法,老年代采用标记-整理算法。
ParNew 收集器(Server 模式下的虚拟机的首要选择)
- Serial 收集器的多线程版本
Parallel(并行) Scavenge 收集器
- 与ParNew差不多,但更关注吞吐量(高效率的利用 CPU),而非用户体验(用户线程的停顿时间)
CMS 收集器(Concurrent(并发) Mark Sweep)(实现了垃圾收集线程与用户线程(基本上)同时工作)
- “标记-清除”四步骤
- 初始标记: 短暂停顿,标记直接与 root 相连的对象(根对象);
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。(并发标记使得用户线程不用冻结,但是使得无法保证可达性分析的实时性)
- 重新标记: 短暂停顿,修正并发标记期间因并发的用户程序导致标记变动的记录。
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。(该阶段已经明确白色结点一定是垃圾,用户线程不可再引用)
- 缺点:1,无法处理浮动垃圾;2,“标记-清除”产生内存碎片
- “标记-清除”四步骤
G1 收集器 (Garbage-First) (面向服务器,具备高吞吐量)
- 运作步骤
- 初始标记: 短暂停顿(Stop-The-World,STW),标记所有从 GC Roots 可直接可达的活跃对象
- 并发标记:与应用并发运行,标记所有可达对象。
- 最终标记: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。
- 筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。(在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region,回收时存在STW)
- 特点
- 并行与并发:cpu多核并行运行,java线程的停顿是并发的短暂停顿
- 分代收集:可独立管理整个 GC 堆,不要求整个 Eden 区、年轻代或者老年代都是连续的,及需固定大小和固定数量。但仍保留了分代的概念。
- 空间整合:从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:建立可预测的停顿时间模型
- 缺点:需要记忆集来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,而且记忆集的维护成本较高,带来了更高的执行负载,影响效率。
- 运作步骤
ZGC 收集器
- 与G1类似,但是ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。
四、字节码(.Class)的文件结构
- 每个 Class 文件的头 4 个字节称为魔数(Magic Number):作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件
- 文件也记录了Class文件版本号(java版本)、类的常量池(记录类/接口的全限定名、字段/方法的名称和描述符等)、类访问标志(public,private等)、字段/方法/属性表集合等一切类有关的信息
五、类加载
1. 类的生命周期:
2. 类加载过程:加载->连接->初始化
- 加载(由该类指向的类加载器实现)
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
- 验证:确保 Class 文件的字节流中包含的信息符合要求,保证JVM安全
- 准备:为类变量分配内存并设置类变量零值(类变量都是静态变量)
- 解析:将常量池内的符号引用替换为直接引用(得到类或者字段、方法在内存中的指针或者偏移量)
- 初始化:加锁保证线程安全(会引起线程阻塞),只有类或其属性被访问,才会主动去初始化类
- 卸载:只有类不存在实例、不被引用、且其类加载器被GC(只有自定义类加载器会被GC),类才会被GC
3. 类加载器
- 加载规则:动态加载(使用时才加载),每个类只会被加载一次
- 只有2个类的全名相同,且其类加载器相同,JVM才认为是同一个类
- JVM 中内置的三个重要的 ClassLoader
- BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null。加载 JDK 内部的核心类库
- ExtensionClassLoader(扩展类加载器):加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类
- AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
- 自定义类加载器:继承 ClassLoader抽象类(一般用组合关系来继承,而非直接继承)
- ClassLoader 类的两个关键的方法
- loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,重写该方法可打破双亲委派机制
- findClass(String name):无法被父类加载器加载的类最终会通过这个方法被加载,默认实现是空方法。继承时重写该方法即可
- ClassLoader 类的两个关键的方法
- 双亲委派模型(每个类加载器优先尝试让父类加载器去加载,保证了一个类只会被一个高级别的类加载器加载)