一.什么是JVM
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
二.JVM运行流程

三.JVM运行时数据区

1.堆(线程共享)
创建出来实例对象会被加载到堆中
2.Java虚拟机栈(线程私有)
虚拟机栈:每个线程都会在虚拟机栈中开辟一个空间,每调用一个方法时,这个方法就会被压入栈中,也就是说每个栈中存放的是方法调用的层级

Java虚拟机栈主要由以下部分构成
- 局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变 量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
- 操作栈:每个方法会生成一个先进后出的操作栈。
- 动态链接:指向运行时常量池的方法引用。
- 方法返回地址:PC 寄存器的地址。
3. 本地方法栈(线程私有)
本地方法栈:同样是每一个线程都会在本地方法栈中开辟一块内存,每次调用一个本地方法,这个本地方法就会被加载到本地方法栈中。
4.程序计数器(线程私有)
程序计数器:记录当前线程所运行到的指令地址:由于考虑到java虚拟机在多线程模式下是通过线程轮流切换并分配时间片的方式进行的,因此当某个线程分配的时间片使用完但是当前线程并没有执行结束时,这时就需要使用程序计数器记录下当前线程所运行到的指令地址,当当前线程再度被分配到时间片时,从当前指令下继续执行。
5.方法区(线程共享)
在运行时数据区中,类对象被加载到方法区中,便于后面new出来的实例对象可以通过这个类对象模板中创建新的对象。

这里都属于内存溢出,注意和内存泄漏区别!!
6.总结各个区域
-
元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
- 方法区:存储类信息、常量、静态变量等数据。它在JVM启动时创建,是所有线程共享的内存区域。
- 堆:存储对象实例和数组。每个线程在创建时会分配一块私有的内存区域,称为TLAB(Thread Local Allocation Buffer),用于分配小对象。
- 栈:存储局部变量和操作栈。每个方法在被调用时,都会在栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态链接信息、方法出口等。
- 程序计数器:存储当前线程执行的字节码的行号指示器。每个线程都有自己的程序计数器,独立存储,互不影响。
- 本地方法栈:为JVM使用到的本地方法(如C语言编写的)服务。
四.JVM类加载
什么是类加载
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
详细的视频讲解:【JVM】Java类加载机制这块算是玩明白了 建议从3:30看

- 加载
- 连接
1. 验证
2. 准备
3.解析 - 初始化
1.加载
加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。
2.验证
验证.class进行语法和语义上的分析,防止对jvm产生危害

3.准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
4.解析
5.初始化
6.双亲委派机制
类加载总共分为以下四种:
- 启动类加载器(Bootstrap Class Loader):它是 JVM 的内部组件,负责加载 Java 核心类库(如java.lang)和其他被系统类加载器所需要的类。启动类加载器是由 JVM 实现提供的,通常使用本地代码来实现。
- 扩展类加载器(Extension Class Loader):它是 sun.misc.Launcher$ExtClassLoader 类的实例,负责加载 Java 的扩展类库(如 java.util、java.net)等。扩展类加载器通常从 java.ext.dirs 系统属性所指定的目录或 JDK 的扩展目录中加载类。
- 系统类加载器(System Class Loader):也称为应用类加载器(Application Class Loader),它是sun.misc.Launcher$AppClassLoader 类的实例,负责加载应用程序的类。系统类加载器通常从 CLASSPATH 环境变量所指定的目录或 JVM 的类路径中加载类。
- 用户自定义类加载器(User-defined Class Loader):这是开发人员根据需要自己实现的类加载器。用户自定义类加载器可以根据特定的加载策略和需求来加载类,例如从特定的网络位置、数据库或其他非传统来源加载类。
双亲委派模型的优点1. 避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。2. 安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。
在什么情况下会选择绕过双亲委派机制?
打破双亲委派模型的方法主要包括
-
重写
loadClass()方法:在双亲委派的过程,都是在loadClass()方法中实现的,因此要想要破坏这种机制,可以自定义一个类加载器,继承ClassLoader并重写loadClass()方法即可,使其不进行双亲委派。 -
利用线程上下文加载器:利用线程上下文加载器(
Thread Context ClassLoader)也可以打破双亲委派。Java 应用上下文加载器默认是使用AppClassLoader。若想要在父类加载器使用到子类加载器加载的类,可以使用Thread.currentThread().getContextClassLoader()。
五.垃圾回收机制
1.死亡对象的判断算法
1.引用计数器算法
2.可达性分析算法
JVM中采用的是此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:

- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native方法)引用的对象。
2.垃圾回收算法
1.标记-清除算法
" 标记 - 清除 " 算法是最基础的收集算法。算法分为 " 标记 " 和 " 清除 " 两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。后续的收集算法都是基于这种思路并对其不足加以改进而已。

2.复制算法
" 复制 " 算法是为了解决 " 标记 - 清理 " 的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :

3.标记-整理算法
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。针对老年代的特点,提出了一种称之为 " 标记 - 整理算法 " 。标记过程仍与 " 标记 - 清除 " 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:

4.分代GC
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区。
随着对象在Eden区越来越多,如果Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC。
Minor GC会把需要eden中和From需要回收的对象回收,把没有回收的对象放入To区。
接下来,SO会变成To区,S1变成From区。当eden区满时再往里放入对象,依然会发生Minor GC.此时会回收eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0。
注意:每次Minor GC中都会为对象记录他的年龄,初始值为0,每次GC完加1。
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代。
当老年代中空间不足,无法放入新的对象时,先尝试minor gc如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收。
如果Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常。

3.垃圾收集器
垃圾收集器的不断更新,目的就是为了减少STW(stop the world)

JVM 常见的垃圾回收器有以下几个:
- Serial/Serial Old:单线程垃圾回收器;
- ParNew:多线程的垃圾回收器(Serial 的多线程版本);
- Parallel Scavenge/Parallel Old:吞吐量优先的垃圾回收器【JDK8 默认的垃圾回收器】;
- CMS:最小等待时间优先的垃圾收集器;
- G1:可控垃圾回收时间的垃圾收集器【JDK 9 之后(HotSpot)默认的垃圾回收器】;
- ZGC:停顿时间超短(不超过 10ms)的情况下尽量提高垃圾回收吞吐量的垃圾收集器【JDK 15 之后默认的垃圾回收器】。

4.垃圾回收的过程
垃圾回收主要是针对堆区进行回收的
新生代:老年代=1:2 eden:S0:S2=8:1:1

1.所有new出来的对象都放在eden区
2.当eden区满了之后,进行一次Minor GC,将存活下来的对象复制到S0区,清除其他区域的对象 (复制算法)
3.继续new对象,直到eden区满了,对eden和S0区进行Minor GC,将存活下来的对象复制到S1区,交换S0和S1区域的位置,清除其他区域的对象(复制算法)
4.重复3的操作,不停的操作直到15次,将存活的对象放到old区.
5.当old区满了之后,对整个区域进行Full GC.
Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝
生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
Full GC(Full Garbage Collection). Full GC 又称为 老年代GC或者Major GC ,是指对整个堆内存进行垃圾回收的过程。在进行 Full GC 时,会对年轻代和老年代(以及永久代或元数据区)中的所有对象进行回收。
Full GC 通常发生在以下情况之一:
- 显式触发:通过调用 System.gc() 方法显式触发垃圾回收。虽然调用该方法只是向 JVM 发出建议,但在某些情况下,JVM 可能会选择执行 Full GC。
- 老年代空间不足:当老年代空间不足时,无法进行对象的分配,会触发 Full GC。此时,Full GC 的目标是回收老年代中的无效对象,以释放空间供新的对象分配。
- 永久代或元数据区空间不足:在使用永久代(Java 8 之前)或元数据区(Java 8 及之后)存储类的元数据信息时,如果空间不足,会触发 Full GC。
Full GC 是一种较为耗时的操作,因为它需要扫描和回收整个堆内存。在 Full GC 过程中,应用程序的执行通常会暂停,这可能会导致较长的停顿时间(长时间的停顿会影响应用程序的响应性能)。 为了避免频繁的 Full GC,通常采取一些优化措施,如合理设置堆大小、调优垃圾回收参数、减少对象的创建和存活时间等。
六.垃圾回收器
主要看这篇文章:Java虚拟机面试题 | 小林coding
垃圾收集器实现
聊完了对象存活判定和垃圾回收算法,接着我们就要看看具体有哪些垃圾回收器的实现了。我们可以自由地为新生代和老年代选择更适合它们的收集器。

Serial收集器
这款垃圾收集器也是元老级别的收集器了,在JDK1.3.1之前,是虚拟机新生代区域收集器的唯一选择。这是一款单线程的垃圾收集器,也就是说,当开始进行垃圾回收时,需要暂停所有的线程,直到垃圾收集工作结束。它的新生代收集算法采用的是标记复制算法,老年代采用的是标记整理算法。

可以看到,当进入到垃圾回收阶段时,所有的用户线程必须等待GC线程完成工作,就相当于你打一把LOL 40分钟,中途每隔1分钟网络就卡5秒钟,可能这时你正在打团,结果你被物理控制直接在那里站了5秒钟,这确实让人难以接受。
虽然缺点很明显,但是优势也是显而易见的:
设计简单而高效。
在用户的桌面应用场景中,内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,使用串行回收器是可以接受的。
所以,在客户端模式(一般用于一些桌面级图形化界面应用程序)下的新生代中,默认垃圾收集器至今依然是Serial收集器。我们可以在java -version中查看默认的客户端模式:
openjdk version "1.8.0_322"
OpenJDK Runtime Environment (Zulu 8.60.0.21-CA-macos-aarch64) (build 1.8.0_322-b06)
OpenJDK 64-Bit Server VM (Zulu 8.60.0.21-CA-macos-aarch64) (build 25.322-b06, mixed mode)
我们可以在jvm.cfg文件中切换JRE为Server VM或是Client VM,默认路径为:
JDK安装目录/jre/lib/jvm.cfg
比如我们需要将当前模式切换为客户端模式,那么我们可以这样编辑:
-client KNOWN
-server IGNORE
ParNew收集器
这款垃圾收集器相当于是Serial收集器的多线程版本,它能够支持多线程垃圾收集:

除了多线程支持以外,其他内容基本与Serial收集器一致,并且目前某些JVM默认的服务端模式新生代收集器就是使用的ParNew收集器。
Parallel Scavenge/Parallel Old收集器
Parallel Scavenge同样是一款面向新生代的垃圾收集器,同样采用标记复制算法实现,在JDK6时也推出了其老年代收集器Parallel Old,采用标记整理算法实现:

与ParNew收集器不同的是,它会自动衡量一个吞吐量(用户线程时间/总时间),并根据吞吐量来决定每次垃圾回收的时间,这种自适应机制,能够很好地权衡当前机器的性能,根据性能选择最优方案。
目前JDK8采用的就是这种 Parallel Scavenge + Parallel Old 的垃圾回收方案。
CMS收集器
在JDK1.5,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发(注意这里的并发和之前的并行是有区别的,并发可以理解为同时运行用户线程和GC线程,而并行可以理解为多条GC线程同时工作)收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
它主要采用标记清除算法:

它的垃圾回收分为4个阶段:
- 初始标记(需要暂停用户线程):这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象,速度比较快,不用担心会停顿太长时间。此阶段需要暂停的原因:GC Roots包括栈帧中的局部变量、全局静态变量等,这些引用是动态变化的。如果不暂停应用线程,应用程序在标记过程中修改了这些引用,垃圾回收器将无法获得一个一致的GC Roots集合,导致无法准确标记出存活对象。
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(需要暂停用户线程):由于并发标记阶段可能某些用户线程会导致标记产生漏标和错标的情况,因此这里需要再次暂停所有线程进行并行标记,这个时间会比初始标记时间长一丢丢。
- 并发清除:最后就可以直接将所有标记好的无用对象进行删除,因为这些对象程序中也用不到了,所以可以与用户线程并发运行。
标记清除算法会产生大量的内存碎片,导致可用连续空间逐渐变少,长期这样下来,会有更高的概率触发Full GC,可以通过设置一XX:CMSFullGcsBeforeCompaction=N设置在N次fullgc后进行整理内存
并且在与用户线程并发执行的情况下,也会占用一部分的系统资源,导致用户线程的运行速度一定程度上减慢。
可能会产生退化问题,当老年代内存进行回收之后还是不足,会退化为单线程的垃圾回收器,此时堆内存已经很大了,会停顿很长的时间。
产生浮动垃圾,在并发清除的阶段,可能会产生不再使用的对象,此时只能等待下次fullgc再进行清理,会对性能产生一定的影响。

Garbage First (G1) 收集器
此垃圾收集器也是一款划时代的垃圾收集器,在JDK7的时候正式走上历史舞台,它是一款主要面向于服务端的垃圾收集器,并且在JDK9时,取代了JDK8默认的 Parallel Scavenge + Parallel Old 的回收方案。
G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的。分为Eden、SurvivoOld区。Region的大小通过堆空间大小/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32M指定,Region size必须是2的指数幂,取值范围从1M到32M。
每一个Region都可以根据需要,自由决定扮演哪个角色(Eden、Survivor和老年代),收集器会根据对应的角色采用不同的回收策略。此外,G1收集器还存在一个Humongous区域,它专门用于存放大对象(一般认为大小超过了Region容量一半的对象为大对象,也可以横跨多个region)这样,新生代、老年代在物理上,不再是一个连续的内存区域,而是到处分布的。

1.老年代回收的算法
它的回收过程与CMS大体类似:

分为以下四个步骤:
- 初始标记(暂停用户线程):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
- 最终标记(暂停用户线程):对用户线程做一个短暂的暂停,用于处理并发标记阶段漏标的那部分对象。
- 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。
2.新生代回收的算法
年轻代回收 (Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数-XX:MaxGCPauseMillis=n(默认200)设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间。

没有到达的放到一个新的survivor区域



3.新生代原理

为什么不直接将卡表的脏卡信息直接同步到记忆集?因为写后屏障指令是由用户线程执行的,如果有大量用户线程直接修改记忆集的数据,会产生线程安全的问题,如果直接加锁用户线程的性能会大大下降。所以我们将脏卡信息同步到脏卡队列中,由指定的线程来更新记忆集的信息。

4.老年代原理



出现的问题:



5.一些参数的设置
-XX:+UseG1GC:启动G1垃圾收集器。-XX:G1HeapRegionSize:设置Region的大小。-XX:MaxGCPauseMillis:设置期望的最大GC停顿时间。-XX:InitiatingHeapOccupancyPercent:设置启动并发标记的阈值。-XX:ConcGCThreads:设置并发标记的线程数。
本文深入介绍了JVM,包括其运行流程、运行时数据区(堆、栈等)、类加载机制及双亲委派机制。还详细阐述了垃圾回收机制,如死亡对象判断算法、垃圾回收算法,以及常见垃圾收集器(Serial、ParNew等)的特点和回收过程,有助于理解JVM的工作原理。

1872

被折叠的 条评论
为什么被折叠?



