java虚拟机:jvm的面试题有这一篇就够了~~~

一、概念

1、JVM是什么

JVM 的全称是 「Java Virtual Machine」,也就是我们耳熟能详的 Java 虚拟机。JVM具备着计算机的基本运算方式,它主要负责把 Java 程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。

JVM是运行在操作系统上的,它与硬件没有直接的交互。

2、java程序的运行过程

Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
在这里插入图片描述

JDK、JRE、JVM

  1. JDK(Java Development Kit Java 开发工具包),JDK 是提供给 Java 开发人员使用的,其中包含了Java 的开发工具,也包括了 JRE。其中的开发工具包括编译工具(javac.exe) 打包工具(jar.exe)等。
  2. JRE(Java Runtime Environment Java 运行环境) 是 JDK 的子集,也就是包括 JRE所有内容,以及开发应用程序所需的编译器和调试器等工具。JRE 提供了**库、Java 虚拟机(JVM)和其他组件,**用于运行 Java编程语言、小程序、应用程序。
  3. JVM(Java Virtual Machine Java 虚拟机),JVM可以理解为是一个虚拟出来的计算机,具备着计算机的基本运算方式,它主要负责把 Java程序生成的字节码文件,解释成具体系统平台上的机器指令,让其在各个平台运行。

二、JVM内存区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
在这里插入图片描述
JDK中各个版本对JVM进行了一些迭代,示意图如下:
在这里插入图片描述
JDK1.6、JDK1.7、JDK1.8 JVM 内存模型主要有以下差异:

  1. JDK 1.6:有永久代,静态变量存放在永久代上。
  2. JDK1.7:有永久代,但已经把字符串常量池、静态变量,存放在堆上。逐渐的减少永久代的使用。
  3. JDK 1.8:无永久代,运行时常量池、类常量池,都保存在元数据区,也就是常说的元空间。但字符串常量池仍然存放在堆上。
1、字符串常量池(String Constant Pool):

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上

2、class常量池(Class Constant Pool)
定义

我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
每个class文件都有一个class常量池

字面量和符号引用
  1. 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  2. 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
3、运行时常量池(Runtime Constant Pool)
  1. 运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。
  2. JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
4、程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

6、Java虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)
在这里插入图片描述

7、本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
Hot-Spot虚拟机直接把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

8、Java堆

从回收内存的角度看,

  • Java 堆,由年轻代和年老代组成,分别占据 1/3 和 2/3。
  • 而年轻代又分为三部分,Eden、From Survivor、To Survivor,占据比例为 8:1:1,可调。
    -在这里插入图片描述
9、方法区(JDK1.8移除)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
到了 JDK 8,终于完全废弃了永久代的概念,改用在本地内存中实现的元空间(Meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

10、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分——在JDK1.8已经被移到了元空间。
运行时常量池相对于Class文件常量池的一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。

11、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制。
元空间从虚拟机 Java 堆中转移到本地内存,默认情况下,元空间的大小仅受本地内存的限制。jdk1.8 以前版本的 class 和 JAR 包数据存储在 PermGen 下面 ,PermGen 大小是固定的,而且项目之间无法共用,公有的 class,所以比较容易出现 OOM 异常。

12、对象分配过程

分配过程

  1. 新生代有一个Eden区和两个survivor区,首先将对象放入Eden区,如果空间不足就向其中的一个survivor区上放,如果仍然放不下就会引发一次发生在新生代的minor GC,将存活的对象放入另一个survivor区中,然后清空Eden和之前的那个survivor区的内存。在某次GC过程中,如果发现仍然又放不下的对象,就将这些对象放入老年代内存里去。
  2. 大对象以及长期存活的对象直接进入老年区。
  3. 当每次执行minorGC的时候应该对要晋升到老年代的对象进行分析,如果这些马上要到老年区的老年对象的大小超过了老年区的剩余大小,那么执行一次Full GC以尽可能地获得老年区的空间。

JVM中的对象

1、对象的创建

Java对象创建的大概过程如下:

在这里插入图片描述

  1. 类加载检查:虚拟机遇到⼀条 new指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
  2. 分配内存:在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种⽅式:选择以上两种⽅式中的哪⼀种,取决于 Java 堆内存是否规整。⽽ Java 堆内存是否规整,取决于 GC收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。

  1. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值
  2. 设置对象头:初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
  3. 执⾏ init ⽅法:在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
在这里插入图片描述

对象的访问定位

Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:
1、句柄:如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息。
在这里插入图片描述
2、直接指针:如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。
在这里插入图片描述

四、GC垃圾回收

对于垃圾回收,主要考虑的就是完成三件事:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?
1、如何判断对象需要回收?
1.1 引用计数法的算法:
  1. 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
  2. 当引用失效时,计数器值就减一;
  3. 任何时刻计数器为零的对象就是不可能再被使用的。
    在Java 领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,例如在处理处理一些相互依赖、循环引用时非常复杂。
1.2 可达性分析算法

这个算法的基本思路就是通过一系列称为**“GC Roots”的根对象**作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
在这里插入图片描述
GC Roots 包括;

  • 全局性引用,对方法区的静态对象、常量对象的引用
  • 执行上下文,对 Java 方法栈帧中的局部对象引用、对 JNI handles 对象引用
  • 已启动且未停止的 Java 线程
1.3 引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。

Java的引用分为四种:强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java提供提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java提供了WeakReference类来实现弱引用。
  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。Java提供了PhantomReference类来实现虚引用。
2、垃圾收集算法
2.1 标记-清除算法

最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。
它的主要缺点有两个:

  • 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
  • 第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

标记-清除算法的执行过程如图:
在这里插入图片描述

2.2、标记-复制算法

标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题。

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费较多。
在这里插入图片描述

2.3 、标记-整理算法

标记-整理算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
在这里插入图片描述

2.4 分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
    基于这两个假说,收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。

设计者一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
基于这种分代,老年代和新生代具备不同的特点,可以采用不同的垃圾收集算法。

  1. 新⽣代中,每次收集都会有⼤量对象死去,所以可以选择标记-复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
  2. ⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以必须选择标记-清除或标记-整理算法进⾏垃圾收集。

因为有了分代收集理论,所以就有了了“Minor GC(新⽣代GC)”、“Major GC(⽼年代GC)”、“Full GC(全局GC)”这样的回收类型的划分

3 垃圾收集器

3.1、Serial收集器

这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
Serial/Serial Old收 集器的运行过程如下:
在这里插入图片描述

3.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-整理

在这里插入图片描述

3.3 Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器

Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

3.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法
Serial Old收集器的工作过程如图所示。
在这里插入图片描述

3.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现
在这里插入图片描述

3.6、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

Concurrent Mark Sweep收集器运行过程如图:
在这里插入图片描述
CMS收集器是基于“标记-清除”算法实现的,主要分为4个步骤。

  1. 初始标记(CMS inital mark):需要“stop the world”,但只标记一下GC Roots能直接关联的对象,速度很快。
  2. 并发标记(CMS concurrent mark):是GC Roots Tracing的过程,花费时间长
  3. 重新标记(CMS remark):*需要“stop the world”,是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  4. 并发清除(CMS concurrent sweep):是并发清除无用对象。
3.7 G1收集器
3.7.1 G1的收集过程可能有4个阶段:
  1. 新生代GC
  2. 并发标记周期
  3. 混合收集
  4. (如果需要)进行Full GC。
3.7.2 G1的新生代GC

新生代GC的主要工作是回收eden区和survivor区。
一旦eden区被占满,新生代GC就会启动。新生代GC收集前后的堆数据如下图所示,其中E表示eden区,S表示survivor区,O表示老年代。

在这里插入图片描述
可以看到,新生代GC只处理eden和survivor区,回收后,所有的eden区都应该被清空,而survivor区会被收集一部分数据,但是应该至少仍然存在一个survivor区,类比其他的新生代收集器,这一点似乎并没有太大变化。另一个重要的变化是老年代的区域增多,因为部分survivor区或者eden区的对象可能会晋升到老年代。

3.7.3 G1并发标记周期

G1的并发阶段和CMS有些类似,它们都是为了降低一次停顿时间,而将可以和应用程序并发执行的部分单独提取出来执行。

  1. 初始标记:标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序在这个阶段必须停止执行。
  2. 根区域扫描:由于初始标记必然会伴随一次新生代GC,所以在初始化标记后,eden被清空,并且存活对象被移到survivor区。在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。这个过程是可以和应用程序并发执行的。但是根区域扫描不能和新生代GC同时发生(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),故如果恰巧此时需要新生代GC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代GC的时间就会延长。
  3. 并发标记:和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发过程,并且这个过程可以被一次新生代GC打断。
  4. 重新标记:和CMS一样,重新标记也是会使应用程序停顿,由于在并发标记过程中,应用程序依然运行,因此标记结果可能需要修正,所以在此阶段对上一次标记进行补充。在G1中,这个过程使用SATB(Snapshot-At-The-Begining)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
  5. 独占清理:顾名思义,这个阶段会引起停顿。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息
  6. 并发清理阶段:识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。

SATB全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。灰:对象被标记了,但是它的field还没有被标记或标记完。黑:对象被标记了,且它的所有field也被标记完了。
SATB 利用 write barrier 将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根 Stop The World 地重新扫描一遍即可避免漏标问题。 因此G1 Remark阶段 Stop The World 与 CMS了的remark有一个本质上的区别,那就是这个暂停只需要扫描有 write barrier 所追中对象为根的对象, 而 CMS 的remark 需要重新扫描整个根集合,因而CMS remark有可能会非常慢。

3.7.4 混合回收

在并发标记周期中,虽有部分对象被回收,但是回收的比例是非常低的。但是在并发标记周期后,G1已经明确知道哪些区域含有比较多的垃圾对象,在混合回收阶段,就可以专门针对这些区域进行回收。当然G1会优先回收垃圾比例较高的区域(回收这些区域的性价比高),这正是G1名字的由来(Garbage First Garbage Collector:译为垃圾优先的垃圾回收器),这里的垃圾优先(Garbage First)指的是回收时优先选取垃圾比例最高的区域。

这个阶段叫做混合回收,是因为在这个阶段,即会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收,同时处理了新生代和老年代。

混合回收会被执行多次,直到回收了足够多的内存空间,然后,它会触发一次新生代GC。新生代GC后,又可能会发生一次并发标记周期的处理,最后又会引起混合回收,因此整个过程可能是如下图:
在这里插入图片描述

3.7.5 必要时的Full GC

并发收集让应用程序和GC线程交替工作,因此在特别繁忙的情况下无可避免的会发生回收过程中内存不足的情况,当遇到这种情况,G1会转入一个Full GC 进行回收。
1、并发模式失效
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

GC日志如下的示例:
在这里插入图片描述

解决办法:发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整周期,让它运行得更快(如,增加后台处理的线程数)。

2、晋升失败
(to-space exhausted或者to-space overflow)
G1收集器完成了标记阶段,开始启动混合式垃圾回收,清理老年代的分区,不过,老年代空间在垃圾回收释放出足够内存之前就会被耗尽。(G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用),由此触发了Full GC。
下面日志中(可以在日志中看到(to-space exhausted)或者(to-space overflow)),反应的现象是混合式GC之后紧接着一次Full GC。

在这里插入图片描述
解决这种问题的方式是:

  1. 增加 -XX:G1ReservePercent选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
  2. 通过减少-XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
  3. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

3、疏散失败
(to-space exhausted或者to-space overflow)

进行新生代垃圾收集是,Survivor空间和老年代中没有足够的空间容纳所有的幸存对象。这种情形在GC日志中通常是:

这条日志表明堆已经几乎完全用尽或者碎片化了。G1收集器会尝试修复这一失败,但可以预期,结果会更加恶化:G1收集器会转而使用Full GC。

解决这种问题的方式是:

  1. 增加 -XX:G1ReservePercent选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。

    通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。

    也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

G1收集器运行过程如图:
在这里插入图片描述

4、Humongous Object 分配失败

当Humongous Object 找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

对于Humongous Object 的处理还有一种方式就是切换GC算法到ZGC,因为ZGC中对于Humongous Object 的回收不会特殊处理(比如不会延迟收集)。

六、巨型对象
Humongous Object:巨型对象 Humongous regions:巨型区域

对于G1而言,只要超过regin大小的一半,就被认为是巨型对象。巨型对象直接被分配到老年代中的“巨型区域”。这些巨型区域是一个连续的区域集。StartsHumongous 标记该连续集的开始,ContinuesHumongous 标记它的延续。

在分配巨型对象之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 Full GC。

对于巨型对象,有以下几个点需要注意:

没有被引用的巨型对象会在标记清理阶段或者Full GC时被释放掉。

为了减少拷贝负载,只有在Full GC的时候,才会压缩大对象region。

每一个region中都只有一个巨型对象,该region剩余的部分得不到利用,会导致堆碎片化。

如果看到由于大对象分配导致频繁的并发回收,需要把大对象变为普通的对象,建议增大Region size。(或者切换到ZGC)

4.类加载过程

4.1、类加载过程

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
过程如下图:
在这里插入图片描述
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:

  • 1)通过一个类的全限定名来获取定义此类的二进制字节流。
  • 2)将这个字节流所代表的静态存储结构转化为堆的运行时数据结构。
  • 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    验证:
    验证阶段大致上会完成四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
    准备:
    准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
    解析:
    解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
4.2 类加载器

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

4.2.1 类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

4.2.2 双亲委派模型
  1. 启动类加载器(Bootstrap Class Loader): 这个类加载器负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
  2. 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
  3. 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径
    (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
4.2.3 双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
在这里插入图片描述
好处:

  1. 一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
  2. 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。
    在这里插入图片描述
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值