无虚拟机不Java,JVM必备核心知识,从入门到大厂

无虚拟机不Java,JVM必备核心知识,从入门到大厂

前言:

JVM 的内存模型和 JVM 的垃圾回收机制一直是 Java 业内从业者绕不开的话题(实际调优、面试)JVM是java中很重要的一块知识,也是面试常问的问题之一.

虚拟机概念:指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统

JVM 内存划分与内存溢出异常

概述

如果在大学里学过或者在工作中使用过 C 或者 C++ 的读者一定会发现这两门语言的内存管理机制与 Java 的不同。在使用 C 或者 C++ 编程时,程序员需要手动的去管理和维护内存,就是说需要手动的清除那些不需要的对象,否则就会出现内存泄漏与内存溢出的问题。

如果你使用 Java 语言去开发,你就会发现大多数情况下你不用去关心无用对象的回收与内存的管理,因为这一切 JVM 虚拟机已经帮我们做好了。了解 JVM 内存的各个区域将有助于我们深入了解它的管理机制,避免出现内存相关的问题和高效的解决问题。

引出问题

在 Java 编程时我们会用到许多不同类型的数据,比如临时变量、静态变量、对象、方法、类等等。 那么他们的存储方式有什么不同吗?或者说他们存在哪?

运行时数据区域

Java 虚拟机在执行 Java 程序过程中会把它所管理的内存分为若干个不同的数据区域,各自有各自的用途。

无虚拟机不Java,JVM必备核心知识,从入门到大厂

(图片来源于网络)

  • 程序计数器线程私有的,可以看作是当前线程所执行字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。这时唯一一个没有规定任何 OOM 异常的区域。
  • 虚拟机栈虚拟机栈也是线程私有的,生命周期与线程相同。栈里面存储的是方法的局部变量、对象的引用等等。在这片区域中,规定了两种异常情况,当线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。当虚拟机栈动态扩展无法申请到足够的内存时会抛出 OOM 异常。
  • 本地方法栈和虚拟机栈的作用相同,只不过它是为 Native 方法服务。HotSpot 虚拟机直接将虚拟机栈和本地方法栈合二为一了。
  • 堆堆是 Java 虚拟机所管理内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动时创建。这个区域唯一的作用就是存放对象实例,也就是 NEW 出来的对象。这个区域也是 Java 垃圾收集器的主要作用区域。当堆的大小再也无法扩展时,将会抛出 OOM 异常。
  • 方法区方法区也是线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量等等。当方法区无法满足内存分配需求时,会抛出 OOM 异常。这个区域也被称为永久代。

垃圾回收算法与收集器

概述

上一篇文章我们已经了解了 Java 的这几块内存区域。对于垃圾回收来说,针对或者关注的是 Java 堆这块区域。因为对于程序计数器、栈、本地方法栈来说,他们随线程而生,随线程而灭,所以这个区域的内存分配和回收可以看作具备确定性。对于方法区来说,分配完类相关信息后内存大小也基本确定了,加上在 JAVA8 中引入的元空间,所以这个部分也不用关注。

方法区回收

很多人认为方法区是没有垃圾收集的,Java 虚拟机规范也确实说过可以不要求在虚拟机方法区实现垃圾收集,而且在这个地方收集性价比比较低。在堆中,一次可以回收70%~95%的空间,而方法区也就是永久代的回收效率远低于此。方法区垃圾收集主要回收两部分内容:废弃常量和无用的类。

JAVA8 引入的元空间很好的解决了方法区回收效率低下的问题。

引出问题

Java 堆中存储的是 NEW 出来的对象,那么什么样的对象是需要被垃圾回收器回收掉的那?可能你会回答不用的对象或者死掉的对象。那如何判断对象已经不用了或者死掉了那?怎么回收这些死掉了的对象那?

如何判断对象已死

  • 引用计数器每当有一个地方引用它时,计数器值就加一,引用失效时,计数器值减一。简单高效,但是没办法解决循环引用的问题。
  • 可达性分析算法这个算法的基本思路就是通过一系列名为 GC ROOTS 的对象作为起始点,从这些节点开始向下搜索。当一个对象到 GC ROOTS 没有任何引用链接时,则证明此对象时不可用的。可以作为 GC ROOTS 的对象包括下面几种:
  1. 方法里引用的对象。
  2. 方法区中的类静态属性引用的对象。
  3. 方法区中的常量引用的对象。
  4. 本地方法中引用的对象。

HotSpot 虚拟机采用的是可达性分析算法。

如何回收

当前的商业虚拟机的垃圾收集都采用分代垃圾回收的算法,这种算法并没有什么新的思想。只是根据对象的存活周期将不同的内存划分为几块。一般是把 Java 堆分为新生代和老年代,根据新生代和老年代存活时间的不同采取不同的算法,使虚拟机的 GC 效率提高了很多。新生代采用复制算法,老年代采用标记-清除或者标记-整理算法。

回收算法

  • 标记-清除算法分为标记和清除两个阶段,首先要标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点:效率问题,标记和清除过程的效率都不高,另外会有不连续的内存碎片。

无虚拟机不Java,JVM必备核心知识,从入门到大厂

  • 复制为了解决效率问题,复制算法出现了,将内存按容量划分为大小相等的两块,每次只使用其中的一块。清除后将活着的对象复制到另外一块上面。简单高效。现在的商业虚拟机都采用这种收集算法来回收新生代。因为新生代中的对象98%都是朝生夕死的,所以并不需要按1:1划分内存,而是按8:1:1分为 Eden,survivor,survivor。每次只使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 上。当 Survivor 空间不够用时,需要依赖老年代进行分配担保。比较适合需要清除的对象比较多的情况。

无虚拟机不Java,JVM必备核心知识,从入门到大厂

  • 标记-整理标记-整理算法和标记-清除算法的标记过程一样,后序有一个对内存进行整理的动作。和标记-整理算法一样,比较适合要清除对象不多的情况。复制算法在对象存活率较高时就要执行较多的复制操作,效率会变的很低。而且如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对对象 100% 存活的极端情况,所以老年代一般不选复制算法,而选择标记-清除或者标记-整理算法。

无虚拟机不Java,JVM必备核心知识,从入门到大厂

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。下面介绍基于 HotSpot 虚拟机中的垃圾收集器。对于垃圾收集器,大家有个概念就可以了,没有必要去深究垃圾收集器的底层原理,当然如果有余力,了解底层原理当然是最好的。

无虚拟机不Java,JVM必备核心知识,从入门到大厂

  • Serial 收集器最早的垃圾收集器,回收新生代,单线程。这里的单线程不仅仅说明它只会使用一个 CPU 或者一条收集线程去完成垃圾收集工作,重要的是,在进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)。
  • ParNew 收集器新生代垃圾回收,ParNew 收集器其实就是 Serial 收集器的多线程版本,在收集算法,Stop The World 和对象分配规则,回收策略上都与 Serial 相同。ParNew 在单核甚至双核 CPU 上的表现不如 Serial,更多的 CPU 才能体现他的优点。
  • Parallel Scanvnge 收集器新生代垃圾回收,采用复制算法,关注吞吐量,不关注停顿时间。停顿时间越短就越适合需要于用户交互的程序,良好的响应速度能提升用户的体验。高吞吐量则可以最高效率地利用 CPU 时间,尽快完成运算任务,适合在后台运算而不需要太多交互的任务。
  • Serial Old 收集器Serial 的老年代版本,单线程,使用标记-整理算法。
  • Parallel Old 收集器Parallel New 的老年代版本,使用标记-整理算法。
  • CMS 收集器CMS 是一种以获取最短回收停顿时间为目标的收集器,注重响应速度。基于标记-清除算法实现的。不同于其他收集器的全程 Stop The World,CMS 会有两次短暂的 Stop The World,垃圾收集和工作线程并发执行。整个过程分为 4 个步骤:
  1. 初始标记(Stop The World),标记 GC Roots 能关联到的对象。
  2. 并发标记
  3. 重新标记(Stop The World)
  4. 并发清除
  • G1 收集器基于标记-整理实现。可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,新生代和老年代都可以回收。

虚拟机中的类加载机制

概述

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

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备和解析三个部分统称为连接。

无虚拟机不Java,JVM必备核心知识,从入门到大厂

(图片来源于网络)

  • 加载:加载是类加载的第一个阶段,这个阶段,首先要根据类的全限定名来获取定义此类的二进制字节流,将字节流转化为方法区运行时的数据结构,在 Java 堆生成一个代表这个类的 java.lang.class 对象,作为方法区的访问入口。
  • 验证:这一步的目的时确保 Class 文件的字节流包含的信息符合当前虚拟机的要求。
  • 准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都会在方法区中进行分配。仅仅是类变量,不包括实例变量。public static int value = 123; 变量在准备阶段过后的初始值为0而不是123,123的赋值要在变量初始化以后才会完成。
  • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 初始化:初始化是类加载的最后一步,这一步会根据程序员给定的值去初始化一些资源。

类加载器

虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到 Java 虚拟机外部去实现,以便让程序自己去决定如何获取所需要的类,这个动作的代码模块称为类加载器。

对于一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,比较两个类是否相等需要在这两个类是由同一个类加载器加载的前提下才有意义。

双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。只有当父类加载器反馈自己无法完成这个加载请求(他的搜索范围中没有找到所需的类)时,子类加载器才会尝试去加载。

好处:使用双亲委派模型的好处是,Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,比如 java.lang.Object,它存放在 rt.jar 中,无论哪一个类加载器要加载这个类,最后都是委派给启动类加载器进行加载。

如果不使用双亲委派模型,用户自己写一个 Object 类放入 ClassPath,那么系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无从保证。

现在你可以尝试自己写一个名为 Object 的类,可以被编译,但永远无法运行。因为最后加载时都会先委派给父类去加载,在 rt.jar 搜寻自身目录时就会找到系统定义的 Object 类,所以你定义的 Object 类永远无法被加载和运行。

Java 虚拟机的类加载器可以分为以下几种:

无虚拟机不Java,JVM必备核心知识,从入门到大厂

(图片来源于网络)

  • 启动类加载器(Bootstrap ClassLoader):这个类负责将 \lib 目录中的类库加载到内存中,启动类加载器无法被Java程序直接饮用。
  • 扩展类加载器(Extension ClassLoader):负责加载 \lib\ext 目录中的类。开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器是 ClassLoader 中 getSystemClassLoader() 方法的返回值,所以一般称为系统类加载器。如果没有自定义过加载器,一般情况下这个就是默认的类加载器。
  • 自定义类加载器(User ClassLoader):通过自定义类加载器可以实现一些动态加载的功能,比如 SPI。

Java 内存模型与线程

概述

了解 JVM 的 Java 内存模型以及结构对于我们在多线程开发时有很大帮助。了解线程安全的虚拟机底层运作原理以及虚拟机实现高效并发所采取的一些列锁优化措施是我们开发高效和安全代码的基础。

通过硬件类比 Java 内存模型

  • 硬件效率一致性计算机的存储设备(内存,磁盘)和处理器的运算速度有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存和处理器之间的缓冲。将运算所需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后在从缓存同步回内存中,这样处理器就无需等待缓慢的内存读写了。这时会有缓存一致性问题,在多处理器系统中,每个处理器都有自己的高速缓存,他们又共享同一主内存,会有可能导致各自的缓存数据不一致的问题。为了解决这个问题,需要根据一些读写协议来操作,比如 MSI、MESI、MOSI、Synapse 等等。(图片来源于网络)在硬件系统中,为了保证处理器内部的运算单元被充分利用,处理器可能会对输入代码进行乱序执行优化。Java 虚拟机即时编译器也有类似的指令重排序优化。
  • Java 内存模型Java 虚拟机规范中试图定义一种 Java 内存模型( Java Memory Model )来屏蔽掉各种硬件和操作系统的内存访问差异,让 Java 在各种平台下都能达到一致的并发效果。Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量是指实例字段,静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为后者是线程私有的,不会被共享。(图片来源于网络)Java 内存模型规定了所有变量都是存储在主内存(Main Memory)中。每条线程还有自己的工作内存,工作内存中保存了被改线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。线程间的通信要通过工作内存进行。工作内存中更改的变量会不定时刷新到主存中。

通过对比发现,二者的变量更改、数据共享、内存刷新以及架构都非常相似。

读者福利:

JVM是Java业内从业者很重要的一个核心点,刚好你在学习Jvm相关的知识,刚好我在分享学习资料。

文章中有一张垃圾收集的学习思维导图,需要领取这张思维导图的高清图的,以及视频学习资料的,私信我 JVM 就可以免费领取啦

由于篇幅限制,jvm知识核心的学习思维导图我就不一一分享,需要的可以关注我的公众号 Java周某人 哦

无虚拟机不Java,JVM必备核心知识,从入门到大厂

无虚拟机不Java,JVM必备核心知识,从入门到大厂

发布了178 篇原创文章 · 获赞 29 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/Javazhoumou/article/details/102941633
今日推荐