JVM 讲解

1 篇文章 0 订阅

目录

1. JVM 运行流程

2. JVM 基本组成(重要)

2.1 堆(线程共享)

2.2 Java 虚拟机栈(线程私有)

2.3 本地方法栈(线程私有)

2.4 程序计数器(线程私有)

2.5 方法区(线程共享) 

3. OOM

4. JVM 垃圾回收算法(理解)

4.1 死亡对象的判断算法

4.1.1 引用计数器算法

4.1.2 可达性分析算法 

4.2 垃圾回收算法

 4.2.1 标记 - 清除算法

4.2.2 复制算法

4.2.3 标记 - 整理算法

4.2.4 分代算法

4.3 垃圾收集器 

 4.3.1 Serial收集器(新生代收集器,串行GC)

4.3.2 ParNew收集器(新生代收集器,并行GC)

4.3.3 Parallel Scavenge收集器(新生代收集器,并行GC)

4.3.4 Serial Old收集器(老年代收集器,串行GC)

4.3.5 Parallel Old收集器(老年代收集器,并行GC)

4.3.6 CMS收集器(老年代收集器,并发GC)

4.3.7 G1收集器(唯一一款全区域的垃圾回收器)

4.3.8 CMS收集器和G1收集器的区别

5. JVM 类加载

5.1 类加载过程

5.2 双亲委派模型

5.2.1 什么是双亲委派模型

5.2.2  双亲委派模型的优点

5.2.3 双亲委派模型的缺点(MS)

6.掌握JMM

6.1 主内存与工作内存

6.2 内存见交互操作

6.3 voliate 型变量的特殊规则


1. JVM 运行流程

JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。

虚拟机是指通过 软件模拟 的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。 JVM 是现实当中不存在的计算机。

JVM 的运行 实际上就是在运行程序中main方法的执行过程。

第一步:程序在执行前,会把 java 代码转换成字节码 class 文件。

第二步:JVM 将 class 文件 通过类加载器加载到内存中的运行时数据区

第三步:JVM 将符合自身具有规范的class 文件通过 执行引擎 将该 class 文件翻译成底层系统指令

第四步:通过调用其他语言的接口 本地库接口 将该指令交给CPU处理

JVM被称虚拟机,它具有一套自己的指令集以及一套class 文件的规范,范式符合该规范的class文件都可以被JVM执行。 

JVM 胡根据不同平台将字节码文件解释成为特定的机器码,从而实现 java 跨平台

2. JVM 基本组成(重要)

(MS)JVM 组成:

2.1 堆(线程共享)

程序中创建的所有对象都保存在堆中。 

JVM 特定平台参数设置: 

-Xms : 最小启动内存;(ms memory start 简称

-Xmx: 最大运行内存;(mx memory max 的简称

-XX:标准参数

堆里面分为两个区域:新生代老生代

新生代存放新建的文件 ,当经过一次GC 次数之后还活着的对象会放入老生代。

文件比较大时,直接放在老生代里,因为后面若仍要使用这个软件,频繁的添加删除,会耗费大量的资源。

新生代还有 3 个区域:一个 Endn + 两个 Survivor S0/S1 )。

垃圾回收的时候会将 Endn 中存活的对象放到一个未使用的 Survivor 中,并把当前的 Endn 和正在使用的 Survivor 清楚掉。

2.2 Java 虚拟机栈(线程私有)

Java 虚拟机栈的 生命周期和线程相同 Java 虚拟机栈描述的是 Java 方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧 (Stack Frame )用于存储局部变量表、操作数
栈、动态链接、方法出口等信息。常说的 栈内存指的就是虚拟机栈
1. 局部变量表 : 存放了编译器可知的各种基本数据类型 (8 大基本数据类型 ) 、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
2. 操作栈 :每个方法会生成一个先进后出的操作栈。
3. 动态链接 :指向运行时常量池的方法引用。
4. 方法返回地址 PC 寄存器的地址。

 什么是线程私有?

由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时 刻,一个处理器 ( 多核处理器则指的是一个内核 ) 都只会执行一条线程中的指令。因此为了切换线程后能 恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存 储。我们就把类似这类区域称之为 " 线程私有 " 的内存。

  

2.3 本地方法栈(线程私有)

本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用 的,而 本地方法栈是给本地方法使用 的。

2.4 程序计数器(线程私有)

 程序计数器的作用:用来记录当前线程执行的行号的。

程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。

如果当前线程正在执行的是一个 Java方法 ,这个计数器记录的是 正在执行的虚拟机字节码指令的地址
如果正在执行的是一个 Native方法 ,这个 计数器值为空

注意:程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM情况的区域 

2.5 方法区(线程共享) 

用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。
JDK 7 时此区域 叫做 永久代 JDK 8 中叫做 元空间

JDK 1.8 元空间的变化: 

        1. 对于 HotSpot 来说, JDK 8 元空间 的内存属于 本地内存 ,这样元空间的大小就不在受  JVM 最大内存的参数影响了,而是与 本地内存的大小有关
        2. JDK 8 中将 字符串常量池 移动到了 中。

运行时常量池: 

运行时常量池是方法区的一部分,存放 字面量 符号引用
字面量 : 字符串 (JDK 8 移动到堆中 ) final 常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

3. OOM

OOM 即就是“Out of Memory”,内存用完了。当JVM没有足够的内存来为对象分配内存空间并且垃圾回收器也已经没有空间可以回收时,就会抛出error。

为什么会出现OOM?

(1)分配的少:虚拟机本身可以使用的内存(一般通过启动时的VM参数指定)太少。

(2)应用用的太多,并且用完了没有释放,此时就造成了内存泄漏或者内存溢出

内存泄漏:申请使用的内存没有完全释放,导致虚拟机不能再次使用该内存,申请者不用了,又不能让虚拟机分配给别的人使用,此时这块内存就泄露了。

内存溢出:申请的内存超出了JVM 能提供的内存大小,此时称为内存溢出。

 最常见的OOM 三种情况:

(1)Java 堆内存溢出:“java.lang.OutOfMemoryError” ,进一步提示“Java heap space” ,Java堆内存溢出是最常见的内存溢出情况。此时要分析是出现了内存泄漏还是内存溢出。

(2)Java 方法区溢出(Java 永久代溢出):“java.lang.OutOfMemoryError”,进一步提示“PermGen space”一般出现于大量class 或者jsp 页面,这些情况下会产生大量的class信息存储于方法区。此种情况可以通过更该方法区大小来解决。过多的常量尤其是字符串也会导致方法区溢出。

(3)Java 虚拟机栈溢出: “java.lang.StackOverflowError” ,比较常见的Java 内存溢出。关于虚拟机栈可能产生的两种异常:a. 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出Stack Overflow异常。b. 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM 异常

4. JVM 垃圾回收算法(理解)

 对于程序计数器虚拟机栈本地方法栈,他们的生命周期与相关线程有关,随线程而生随线程而灭。这三个区域在方法结束或者线程结束时,内存会跟着线程回收

4.1 死亡对象的判断算法

4.1.1 引用计数器算法

给对象增加一个 引用计数器 ,每当有一个地方 引用 它时, 计数器就+1 ;当引用 失效 时, 计数器就-1 ;任何时刻 计数器为0 的对象就是不能再被使用的,即对象 已"死"
缺点:存在循环引用,比如,对象1调用对象2,对象2又指向对象1 。(主流的JVM不使用计数法来管理内存)
 System.gc();  // 强制jvm进行垃圾回收

4.1.2 可达性分析算法 

通过一系列称为 "GC Roots"  的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为 " 引用链 " ,当一个对象到 GC Roots 没有任何的引用链相连时 ( GC Roots 到这个对象不可达) 时,证明此对象是不可用的。
主流判断对象是否存活的算法是可达性分析算法。

 上图中,Object 5、6、7之间有关联,但是他们无法到达GC Roots ,因此他们是可回收对象。

可作为GC Roots 的对象组成(MS)

        1. 栈中(局部变量)引用的对象。

        2. 方法区(元空间)中静态变量引用的对象。

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

        4. 本地方法栈中引用的对象。

在JDK 1.2 ,对引用做了扩展,将引用分为强引用、软引用、弱引用、虚引用。强度依次递减。

1. 强引用 强引用指的是在程序代码之中普遍存在的,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。(GC的时候不会回收)

2. 软引用软引用是用来描述一些还有用但是不是必须的对象。在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。(在内存溢出之前回收)

3. 弱引用弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。(下一次GC就会被回收)

4. 虚引用通过引用得不到对象,在GC时收到一个通知而已。使用比较少。

4.2 垃圾回收算法

 4.2.1 标记 - 清除算法

 "标记-清除"算法是最基础的收集算法。算法分为"标记""清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

缺点:

1. 标记 和 清除效率都不高

2. 标记清除之后,存在不连续的内存碎片,碎片太多会导致程序在以后运行中需要分配较大的对象时,无法找到连续内存,因此不得不触发另一次垃圾收集

4.2.2 复制算法

 "复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使 用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后 再把已经使用过的内存区域一次清理掉

这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。

优点:性能高 。

缺点:内存利用率低(一般是50%) 。

4.2.3 标记 - 整理算法

标记过程仍与 " 标记 - 清除 " 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都 向一端移动 ,然后 直接清理掉端边界以外的内存

优点:利用所有的内存空间,并不会产生内存碎片。

缺点:效率不高

4.2.4 分代算法

 分代算法和上面讲的 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略, 从而实现更好的垃圾回收。

当前 JVM 垃圾收集都采用的是 "分代收集(Generational Collection)"算法 ,这个算法并没有新思想,只是 根据对象存活周期的不同 将内存划分为几块。一般是把Java 堆分为 新生代 老年代 新生代 中,每 次垃圾回收都有大批对象死去,只有少量存活,因此我们采用 复制算法 ;而 老年代 中对象存活率高、没 有额外空间对它进行分配担保,就必须采用 "标记-清理" 或者 "标记-整理" 算法

哪些对象会进入新生代?哪些对象会进入老年代?

新生代 :一般创建的对象都会进入新生代;
老年代 :大对象和经历了 N 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。

4.3 垃圾收集器 

垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存间。

 4.3.1 Serial收集器(新生代收集器,串行GC)

 是一个单线程的收集器,但它的“单线程的意义并不仅仅说明它只会使用一个CPU或一

条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须 暂停其他所有的工作线程,直到它收集结束
使用的是 复制算法

优势:简单而高效 

4.3.2 ParNew收集器(新生代收集器,并行GC)

 就是Serial收集器的多线程版本,这两种收集器也共用了相当多的代码。复制算法

4.3.3 Parallel Scavenge收集器(新生代收集器,并行GC)

是一个 新生代 收集器 ,它也是使用 复制算法 的收集器,又是 并行 多线程 收集器。

 吞吐量大,停顿时间就小。

Parallel Scavenge收集器 VS ParNew收集器: 

Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别是它具有 自适应调节策略

Parallel Scavenge收集器使用两个参数控制吞吐量:

XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
XX:GCRatio 直接设置吞吐量的大小

4.3.4 Serial Old收集器(老年代收集器,串行GC)

Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。 

4.3.5 Parallel Old收集器(老年代收集器,并行GC)

 Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法。

4.3.6 CMS收集器(老年代收集器,并发GC

 是一种以获取最短回收停顿时间为目标的收集器,重视响应时间,以给用户带来较好的体验。“标记—清除”算法。

 CMS 的运作过程(MS)

1. 初始标记。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。

2. 并发标记。进行GC Roots Tracing的过程。

3. 重新标记

为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”
4.  并发清除 。并发清除阶段会清除对象。

优点: 并发收集、低停顿

缺点:

1. CMS收集器对CPU资源非常敏感 。在并发阶段,它虽然不会导致用户线程 停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

 2. CMS收集器无法处理浮动垃圾。

3. CMS收集器会产生大量空间碎片。基于“标记清除算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

4.3.7 G1收集器(唯一一款全区域的垃圾回收器)

是用在 heap memory 很大的情况下,把 heap 划分为很多很多的 region块,然后并行的对其进行垃圾回收。G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。

 ​​​​​​​

4.3.8 CMS收集器和G1收集器的区别

(73条消息) CMS收集器和G1收集器的区别_技术无产者的博客-CSDN博客_cms收集器和g1收集器

5. JVM 类加载

5.1 类加载过程

 对于一个类来说,它的生命周期是这样的:

前5步是固定的顺序也是类加载的过程。 

(1)加载

(2)连接

        a. 验证

        b. 准备

        c. 解析 

(3)初始化

(1)加载(MS)

“加载” 阶段是 “类加载” 过程的一个阶段,他和类加载是不同的,一个是加载Loading ,一个是类加载 Class Loading。两者不一样。

 加载Loading 阶段,Java 虚拟机需要完成三件事情:

        a. 通过一个类的全限定名(包名 + 类名)来获取定义此类的二进制字节流

        b. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

        c. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

(2)验证

验证是连接阶段的第一步,这一阶段的目的是 确保Class文件的字节 流中包含的信息符合《Java虚拟机规范》的全部约束要求 ,保证这些信 息被当作代码运行后不会危害虚拟机自身的安全。

 验证选项:

        1. 文件格式验证

        2. 字节码验证

        3. 符号引用验证

(3)准备 

准备阶段是正式为类中定义的 变量 (即静态变量,被 static 修饰的变量) 分配内存 并设置 类变量初始值 的阶段。

比如, public static int value = 123; 它的初始值是0,不是123。

(4)解析

解析阶段是 Java 虚拟机 将常量池内的 符号引用 替换为 直接引用 的过程,也就是初始化常量的过程。
(5)初始化
初始化阶段, Java 虚拟机真正开始执行类中编写的 Java 程序代码,将 主导权 移交给 应用程序 。初始化阶段就是 执行类构造器方法 的过程。

5.2 双亲委派模型

从Java虚拟机的角度来说,分为两种类加载器:一种是启动类加载器,另一种是除了启动类加载器之外的所有类加载器。启动类加载器是虚拟机的一部分,由C++实现。另外一种 由Java实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。

从JDK 1.2 以来,Java 一直保持着三层类加载器双亲委派的类加载架构器。 

5.2.1 什么是双亲委派模型

 如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个类委派给父类加载器去执行。因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候,自类加载器才会尝试自己去完成加载。

如果一个父类有多个子类,子类1要收到类加载的请求,将这个 类委派给父类,当第二个类接收到类加载请求时,因为父类已经执行过了,就不用再次执行了,从而避免重复加载父类。 

5.2.2  双亲委派模型的优点

1. 避免重复类加载某个类。

2. 避免系统API 被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己,就会出现问题,比如说,我们编写 java.lang.Object 类,那么程序运行的时候,系统会出现多个不同的Object 类,而这些Object 类又是用户自己提供的,因此不能保证用户写的这个类是正确的。

5.2.3 双亲委派模型的缺点(MS)

(MS) 双亲委派模型缺点(或者说是什么场景会打破双亲委派模型)?

 比如说,Java 中SPI (Service Provider Interface,服务提供接口)机制中的JDBC实现。

SPI 主要解决的是耦合问题,Java 会定义一些规范以及接口,而这些接口的实现可以由第三方实现,只要遵循接口实现规范即可。比如JDBC

JDBC 中的接口是Java 的核心包,DriverManager 位于 rt.jar 包,由 BootStrap 类加载器加载, 而其 Driver 接口的实现类是位于服务商提供的 Jar 包中,是由子类加载器(线程上下文加载器 Thread.currentThread().getContextClassLoader )来加载的,这样就破坏了双亲委派模型了(双亲委 派模型讲的是所有类都应该交给父类来加载,但 JDBC 显然并不能这样实现)。

6.掌握JMM

 (MS)什么是Java 内存模型(JMM) ?

 JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

6.1 主内存与工作内存

 根据JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有的变量都存储在主存中,对于所有线程都是共享的。每个线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝。线程对所有变量的操作都是在工作内存中进行的,线程之间无法直接相互访问变量传递需要通过主存来完成。

6.2 内存见交互操作

将一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存之类的, 主内存和工作内存之间都是有一个具体的交互协议,Java 内存模型中定义了 lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入) 8种操作来完成,这八个操作都具有原子性不可再分

(1) lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
(2) unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3) read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load 动作使用。
(4) load(载入) : 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
(5) use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
(6) assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
(7) store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用。
(8) write(写入) : 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

Java 内存模型的三大特性:

原子性:一个操作不可以中断,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。即就是说,一个程序要么全部执行并在执行的过程中不中断,要么就不执行。

可见性:当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

有序性如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的 操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序""工作内存与主内存同步延迟"现象。

6.3 voliate 型变量的特殊规则

当一个变量定义为 volatile 之后,它将具备两种特性:
第一:保证此变量对所有线程的可见性。

这里的 " 可见性 " 是指 : 当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,普通变量的值在线程间传递均需要通过主内存来完成。

例如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A 回写完成之后再从主内存进行读取操作,新值才会对线程B可见。
第二:使用volatile变量的语义是禁止指令重排序

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值