写个需要烂熟于心的 jvm

基础

Java 虚拟机,是一个可以执行 Java 字节码的虚拟机进程。
Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class )。
跨平台的是 Java 程序(包括字节码文件),而不是 JVM。
JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM 。
写个需要烂熟于心的 jvm

线程共享区:

  1. 本地内存(堆外内存)
    方法区(永久区 元空间):
    当文件被加载时被初始化
    • 被虚拟机加载的类信息
    • 常量 (字符串字面量和数字常量等)
    • 常量池-存放编译期生成的各种字面量和符号引用。
    • 编译器编译后的代码等数据
    • static修饰的静态
    • 异常的定义 OutOfMemoryError

在Java 8之前:静态变量存储在permgen空间(也称为方法区域)。PermGen空间也称为方法区域。PermGen空间用来存储3件东西类级数据(元数据)内串静态变量从Java 8开始静态变量存储在堆本身中,
从Java 8开始,PermGen空间被删除,新的空间被命名为MetaSpace,它不再是堆的一部分,不像以前的Permgen空间。元空间存在于本机内存(操作系统提供给特定应用程序的内存,供其自己使用),现在它只存储类元数据。内部字符串和静态变量被移到堆本身中。

  1. 堆 :(主要的内存工作区域)
    • 存储对象实例
    • 数组
    ① 垃圾回收机制管理的主要区域,新生代老年代OutOfMemoryError
    ② 其大小可通过-Xms和-Xmx来控制
    -Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1GB;
    -Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1GB
    默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小
    通常将-Xms和-Xmx的值设成一样。

线程独占区:

  1. 程序计数器:记录当前线程所执行的字节码文件行号
  2. 虚拟机私有栈:
    • 操作数据栈 计算过程中的变量临时的存储空间
    • 局部变量表 用于报错函数的参数及局部变量
    • 帧数据区 除了局部变量表和操作数据栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着
    对象的引用、基本数据类型、方法参数,返回值
    一个线程的Java栈在线程创建的时候被创建
    每次方法执行都会创建一个栈帧,代码运行完毕自动释放
    入栈出栈的时机很明确,所以这块区域不需要进行 GC。
    局部变量表(存储基本类型数据,引用),栈满时会Stack OverflowError
    虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出 OutOfMemory异常
  3. 本地方法栈:
    native方法服务 JNI java调用其他语言 通过-Xss来指定其大小

执行引擎

虚拟机核心的组件
它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

垃圾回收

1.对象存活判断

1.1 引用计数

每个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。目前在用的有 Python、ActionScript3 等语言。

1.2 可达性分析

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。目前在用的有 Java、C# 等语言。
GC Roots 对象:

虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区中的类静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中 JNI(即一般说的 Native 方法)中引用的对象。

如何判断无用的类:
该类所有实例都被回收(Java 堆中没有该类的对象)。

加载该类的 ClassLoader 已经被回收。

该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方利用反射访问该类。

2.垃圾回收算法

2.1 标记-清除

在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。

缺点:
1、效率问题,标记和清除两个过程的效率都不高。
2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.2 标记-整理

标记整理算法,类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

优点:
1、相对标记清除算法,解决了内存碎片问题。
2、没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。
缺点:
1、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高。

2.3 复制算法

复制算法,可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)

优点:
1、效率高,没有内存碎片。
缺点:
1、浪费一半的内存空间。
2、复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

2.4 分代算法

当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法。而老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。

图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。
对象分配策略:对象优先在 Eden 区域分配,如果对象过大直接分配到 Old 区域。长时间存活的对象进入到 Old 区域。
改进自复制算法
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 2 块 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

3. JVM 垃圾回收器

3.1 新生代

Serial

最基本的单线程垃圾收集器。使用一个CPU或一条收集线程去执行垃圾收集工作。

工作时会Stop The World,暂停所有用户线程,造成卡顿。适合运行在Client模式下的虚拟机。

用作新生代收集器,复制算法。

ParNew

Serial收集器的多线程版本,和Serial的唯一区别就是使用了多条线程去垃圾收集。

除了Serial,只有它可以和CMS搭配使用的收集器。

用作新生代收集器,复制算法。

Parallel Scavenge

用作新生代收集器,复制算法。关注高吞吐量,可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

3.2 老年代

Serial Old

Serial收集器的老年代版本,单线程,标记-整理 算法。

一般用于Client模式的虚拟机。

当虚拟机是Server模式时,有2个用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用 ,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

9.5、Parallel Old(老年代)
Parallel Scavenge收集器的老年代版本,使用多线程和 标记-整理 算法。在JDK 1.6中开始提供。在注重吞吐量的场合,配合Parallel Scavenge收集器使用。

9.6、CMS(Concurrent Mark Sweep)(老年代)
一种以获取最短回收停顿时间为目标的收集器。适合需要与用户交互的程序,良好的响应速度能提升用户体验。

基于 标记—清除 算法。适合作为老年代收集器。

收集过程分4步:

初始标记(CMS initial mark):只是标记一下GC Roots能直接关联到的对象,速度很快,会Stop The World。

并发标记(CMS concurrent mark):进行GC Roots Tracing(可达性分析)的过程。

重新标记(CMS remark):会Stop The -World。为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般比初始标记阶段稍长些,但远比并发标记的时间短。

并发清除(CMS concurrent sweep):回收内存。

耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以时并发执行的。

缺点:

并发阶段,虽然不会导致用户线程暂停,但会占用一部分线程(CPU资源),导致应用变慢,吞吐量降低。默认启动收集线程数是(CPU数量+3)/4。即当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。

无法清除浮动垃圾。并发清除阶段,用户线程还在运行,还会产生新垃圾。这些垃圾不会在此次GC中被标记,只能等到下次GC被回收。

标记-清除 算法会产生大量不连续内存,导致分配大对象时内存不够,提前触发Full GC。

9.7、G1
-XX:G1HeapRegionSize

E:eden区,新生代

S:survivor区,新生代

O:old区,老年代

H:humongous区,用来放大对象。当新建对象大小超过region大小一半时,直接在新的一个或多个连续region中分配,并标记为H

可预测的停顿时间:估算每个region内的垃圾可回收的空间以及回收需要的时间(经验值),记录在一个优先列表中。收集时,优先回收价值最大的region,而不是在整个堆进行全区域回收。这样提高了回收效率,得名:Garbage-First。G1中有2种GC:

young GC:新生代eden区没有足够可用空间时触发。存活的对象移到survivor区或晋升old区。mixed GC:当old区对象很多时,老年代对象空间占堆总空间的比值达到阈值(-XX:InitiatingHeapOccupancyPercent默认45%)会触发,它除了回收年轻代,也回收 部分 老年代(回收价值高的部分region)。

mixed GC回收步骤:

初始标记(Initial Marking):只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这阶段需要停顿线程(STW),但耗时很短,共用YGC的停顿,所以一般伴随着YGC发生。

并发标记(Concurrent Marking):进行可达性分析,找出存活对象,耗时长,但可与用户线程并发执行。

最终标记(Final Marking):修正并发标记阶段用户线程运行导致的变动记录。会STW,但可以并行执行,时间不会很长。

筛选回收(Live Data Counting and Evacuation):根据每个region的回收价值和回收成本排序,根据用户配置的GC停顿时间开始回收。

当对象分配过快,mixed GC来不及回收,G1会退化,触发Full GC,它使用单线程的Serial收集器来回收,整个过程STW,要尽量避免这种情况。

当内存很少的时候(存活对象占用大量空间),没有足够空间来复制对象,会导致回收失败。这时会保留被移动过的对象和没移动的对象,只调整引用。失败发生后,收集器认为存活对象被移动了,有足够空间让应用程序使用,于是用户线程继续工作,等待下一次触发GC。如果内存不够,就会触发Full GC。

9.8、ZGC
在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。
ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么其他阶段是怎么做到可以并发执行的呢?
ZGC主要新增了两项技术,

着色指针Colored Pointer,

读屏障Load Barrier。

ZGC 是一个 并发、基于区域(region)、增量式压缩 的收集器。Stop-The-World 阶段只会在根对象扫描(root scanning)阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增加。
处理阶段:

标记(Marking);

重定位(Relocation)/压缩(Compaction);

重新分配集的选择(Relocation set selection);

引用处理(Reference processing);

弱引用的清理(WeakRefs Cleaning);

字符串常量池(String Table)和符号表(Symbol Table)的清理;

类卸载(Class unloading)

着色指针Colored Pointer
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。

相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于Java术语当中的引用。

在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。

由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),

读屏障Load Barrier
若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。

与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。

其他

1.finalize()

finallize()方法,是在释放该对象内存前由 GC (垃圾回收器)调用。
通常建议在这个方法中释放该对象持有的资源,例如持有的堆外内存、和远程服务的长连接。一般情况下,不建议重写该方法。对于一个对象,该方法有且仅会被调用一次。

2. 对象引用类型

2.1、强引用
如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
2.2、软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
2.3、弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
2.4、虚引用
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

拓展

利用软引用和弱引用解决 OOM 问题。用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题. 通过软引用实现 Java 对象的高速缓存。比如我们创建了一 Person 的类,如果每次需要查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量 Person 对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次 GC 影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能。