JVM常见知识点总结

1.概念

1.1JVM概念

JVM(Java Virtual Machine):Java虚拟机,JVM是一种用于计算设备的规范,他是一个虚构出来的计算机。
问题:Java语言如何实现跨平台(与平台无关性)?
答:引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用JVM屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在JVM上运行的目标代码(字节码),这样每个平台应用其对应的JVM来执行字节码文件。特点:一次编译,多次运行。

1.2常见虚拟机

常见的虚拟机:JVM、VMwave、Virtual Box
JVM与其他二者的区别:

  1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器 。
  2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪 。
  3. JVM是一台被定制过的现实当中不存在的计算机。

2.JVM内存模型介绍

2.1JVM内存区域

Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口、垃圾手机模型

2.2JVM运行时数据区内存模型

在这里插入图片描述

线程私有:Java虚拟机栈、本地方法栈、程序计数器
线程共有:堆、元空间(JDK1.7叫做方法区)

2.3各区域的作用

  • 堆:所有的对象都是存在此区域的,此区域也是JVM中最大对的一块区域。

JVM的垃圾回收针对此区域。

  • Java虚拟机栈(JVM栈):
    • 局部变量表:8大基本数据类型、对象的引用,也就是方法参数和局部变量。
    • 操作栈:每个方法都对应一个先进后出的操作栈。
    • 动态链接:指向运行时常量池的方法引用。
    • 方法返回地址:PC寄存器的地址。
  • 本地方法栈:与Java虚拟机栈类似,但是JVM栈是供Java和JVM使用的,而本地方法栈是为了本地方法(C/C++)服务的。
  • 程序计数器:用来计数线程执行的行号。
  • 元空间:
    • JDK1.7叫做方法区(永久代):运行时常量信息、字符串常量池、类的元信息…
    • JDK1.8元空间:本地内存,并且将字符串常量池移动到堆里面,并将字符串常量池移动到堆中。

2.4运行时常量池

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

2.5JVM参数调优

-Xss:规定了每个线程虚拟机栈的⼤⼩。
-Xms(memory start):表示初始化JAVA堆的⼤⼩及该进程刚创建出来的时候,他的专属JAVA堆的⼤ ⼩,⼀旦对象容量超过了JAVA堆的初始容量,JAVA堆将会⾃动扩容到-Xmx⼤⼩。
-Xmx(memory max):表示java堆可以扩展到的最⼤值,在很多情况下,通常将-Xms和-Xmx设置成 ⼀样的,因为当堆不够⽤⽽发⽣扩容时,会发⽣内存抖动影响程序运⾏时的稳定性。
⼩技巧:⼀般将 -Xms 和 -Xmx 设置为⼀样的可以避免 GC 调整堆⼤⼩所带来的额外压⼒。

3.堆的内存模型

  • 新生代:第一次创建的对象都会分配至此。
  • 老年代:经历了一定次数的垃圾回收之后(默认15GC),依然存活下来的对象会移动到老年代中。并且大对象的创建也会直接进入老年代中。

3.1新生代区域划分

第一次创建的对象都会分配到此区域

  • Eden:80%内存
  • S0:10%内存
  • S1:10%内存

这样的空间分配,可以使新生代的内存利用率达到90%。
问题:为什么大对象会直接进入老年代?
答:核心原因是因为大对象的初始化比较耗时,如果频繁的创建和销毁会带来一定的性能开销,因此最好的实现方式是将它存入GC频率更低的老年代

3.2老年代区域

经历了一定的垃圾回收之后,依然存活下来的对象会移动到老年代,大对象在创建的时候也会直接进入老年代。默认的执行次数为15次,经历15次GC,就会从新生代变为老年代。

4.内存溢出问题(OOM)

4.1Java堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来GC 清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
Java堆内存的OOM异常是实际应用中最常见的内存溢出情况。当出现Java堆内存溢出时,异常堆栈信 息"java.lang.OutOfMemoryError"会进一步提示"Java heap space"。当出现"Java heap space"则很明 确的告知我们,OOM发生在堆上。 此时要对Dump出来的文件进行分析,以MAT为例。分析问题的产生到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
出现的问题:

  1. 内存泄露:泄露对象无法被GC
  2. 内存溢出:内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆 内存调大;或者检查对象的生命周期是否过长。

4.2虚拟机栈和本地方法栈溢出

由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置。
关于虚拟机栈会产生的两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
  2. 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常

5.JVM类加载机制(Class Loading)

在这里插入图片描述

5.1类加载流程

加载(loading) -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
①加载
加载阶段要完成的三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

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

  • 文件格式验证
  • 字节码验证
  • 符号引用验证…

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

public static int value = 123;

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

5.2双亲委派模型

在这里插入图片描述

  • 什么是双亲委派模型?

答:如果⼀个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类 加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类 加载器中,只有当父加载器反馈自己无法法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
简单来说:当加载一个类的时候,那么这个类不会直接加载,而是将这个加载任务直接交给父类。当父类找不到这个类是时候,才自己尝试去加载。
双亲委派模型优点:

  1. 唯一性(父类执行加载一次)
  2. 安全性(会往上面找而上层的类是系统提供的类,避免加载自定义类,从而一定程度上保证了安全性)

破坏双亲委派模型(3次)

  1. JDK1.2提出双亲委派模型,为了兼容老代码,因此在JDK1.2的时候已经出现了破坏双亲委派模型的场景
  2. 是因为双亲委派模型自己的缺点,比如在父类中要调用子类的方法是没办法实现的
  3. 人们对于热更新的追求

6.垃圾回收和分配策略

6.1判别死亡对象

6.1.1引用计数器

引用计数器算法:为每个对象创建一个计数器,当有程序引用此类的时候,计数器+1,当引用失效时,计数器-1,任何时刻当计数器为0时,就说明此对象不能使用,可将它归为死亡对象。
**缺点:**无法解决对象的循环引用问题

6.1.2可达性分析

可达性分析:通过一系列称为GC Roots的对象为起点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象不能通过任何引用链到达GC Roots时(GC Roots到这个对象不可达时),说明此对象是不可用的,以死亡。

  • GC Roots对象:
  1. 虚拟机栈(中的本地变量表)中引用的对象
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈JNI(Native方法)引用的对象

6.1.3引用的分类

在JDK1.2以前,Java中引用的定义很传统 : 如果引用类型的数据中存储的数值代表的是另一块内存的起 始地址,就称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没 有被引用两种状态。
我们希望能描述这一类对象 : 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收 后还是非常紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。
在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

  1. 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类 的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
  2. 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在 系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回 收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来 实现软引用。
  3. 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的 对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是 否够用,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现 弱引用。
  4. 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否 有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实 例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通 知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。

6.1.4对象生存还是死亡

即使在可达性分析算法中不可达的对象,也并非"非死不可"的,这时候他们暂时处在"缓刑"阶段。要宣告 一个对象的真正死亡,至少要经历两次标记过程 : 如果对象在进行可达性分析之后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是正"死"的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列 之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是 虚拟机会触发finalize()方法)。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中 的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个 对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是 没有逃脱,那基本上它就是真的被回收了。

6.2回收方法区

方法区(永久代)的垃圾回收主要收集两部分内容:废弃常量和无用的类。
判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :

  1. 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法

6.3垃圾回算法

6.3.1标记-清除算法

先标记,在清除
标记-清除算法的不足主要有两个 :

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

6.3.2复制算法(新生代的回收算法)

每次使用Eden区域和一个S区域,将还存活的对象赋值到另一个S区域上,然后清理已经使用过的内存区域。这样做的好处是每次都是对整个半区进行内存回收,内存分配 时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运 行高效。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。

  • HotSpot实现的复制算法流程如下:
  1. 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
  2. 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
  3. 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数
    MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

6.3.3标记整理算法(老年代回收算法)

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

6.3.4分代收集算法

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

  • 面试题 : 请问了解Minor GC和Full GC么,这两种GC有什么不一样吗
  1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
  2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行FullGC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

7.垃圾回收器

在这里插入图片描述

Serial收集器(新生代收集器,串行GC)
ParNew收集器(新生代收集器,并行GC)
Parallel Scavenge收集器(新生代收集器,并行GC)
Serial Old收集器(老年代收集器,串行GC)
Parallel Old收集器(老年代收集器,并行GC)
CMS收集器(老年代收集器,并发GC):
是以最短STW为目标的垃圾回收器,主要应用于B/S结构的服务器上。优点:响应速度快。

  1. 初始标记:简单快速的标记GC ROOTS 能够关联到的对象,需要STW
  2. 并发标记:这个阶段是正式的GC ROOTS TRACING阶段,也就是存活对象标记阶段
  3. 重新标记:这个是为了修正并发标记,并发标记期间用户可能做了某些更改,需要STW
  4. 并发清除:清除对象

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从 总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。高并发、低停顿
G1收集器(唯一一款全区域的垃圾回收器):
这是JDK11的默认收集器,会有内存压缩过程,基本不会存在STW。G1是分代的,整体来看是标记-整理,局部来看是复制算法。
新生代GC-并发标记(老年代GC100%)-混合回收-FULL GC(可选)

8.JMM(Java内存模型)

JVM定义了⼀种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差 异,以实现让Java程序在各种平台下都能达到⼀致的内存访问效果。在此之前,C/C++直接使⽤物理硬件 和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,有可能导致程序在⼀套平台上并发 完全正常,而在另⼀套平台上并发访问经常出错。
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中 保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内 存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线 程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示 :
在这里插入图片描述

8.1JVM三大特性

原子性、可见性、有序性

8.2内存空间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存、如何从工作 内存同步回主内存之类的实现细节,Java内存模型中定义了如下8种操作来完成。JVM实现时必须保证下面提及的每一种操作的原子的、不可再分的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值