JVM:基础

整篇 参考学习2022最新Android中高级面试题合集.pdf
一、组成
在这里插入图片描述
JVM包含两个子系统和两个组件,两个子系统为Class loader(类加载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
**Class loader(类加载):**根据给定得全限定名 类名(如java.lang.Object)来装在class文件到Runtime data area中得method area。
**Execution engine(执行引擎):**执行classes中的指令。
**Runtime data area(运行时数据区):**这就是我们常说的JVM的内存。
**Native Interface(本地接口):**与native libraries交互,是其他编程语言交互的接口。

Java程序运行机制详细说明
步骤一:利用IDE集成开发工具编写java源代码,原文及的后缀为.java
步骤二:利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀为.class,里面的一行一行的东西都是JVM指令,java虚拟机跨平台的基础就是这些JVM指令,这些指令在所有平台都是一致的;
步骤三:ClassLoader(类加载器)把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),他就专门负责把每一条JVM指令(比如getstatic)解析成为机器码,机器码就可以交给CPU执行。
步骤四:将底层系统指令交由CPU去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
在这里插入图片描述
二、JVM运行时数据区
java虚拟机规范规定的区域分为5个部分:
在这里插入图片描述

程序计数器:
参考链接:https://blog.csdn.net/PurineKing/article/details/124706926
1、概念:
程序计数器的英文全称是Program Counter Register,又叫程序计数寄存器。Register的命令源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。即在物理上实现程序计数器是通过寄存器来实现的,我们的程序计数器是Java对物理硬件的屏蔽和抽象,他在物理上是通过寄存器来实现的。寄存器可以说是整个CPU组件里读取速度最快的一个单元,因为读取/写指令地址这个动作是非常频繁的。所以,Java虚拟机在设计的时候就把CPU中的寄存器当做了程序计数器,用他来存储地址,将来 去读取这个地址。

2、作用
用于存储下一条指令的地址。详细的说PC寄存器是用来存储指向下一条指令的地址,也就是即将将要执行的指令代码。由执行引擎读取下一条指令。
举一个例子:

//获取java文件的指令码
javac E:\project\android\test\testCustomView\app\src\main\java\com\test\customview\lambda\LambdaSimple.java

//将字节码进行编译生成汇编代码
javap -c -p -v E:\project\android\test\testCustomView\app\src\main\java\com\test\customview\lambda\LambdaSimple.class

在这里插入图片描述
程序计数器的作用就是在一些指令的执行过程中,记住下一条JVM指令的执行地址。比如,上边的例子中,JVM指令前面都有数字,这些数字可以理解成指令对应的地址,当这些指令被虚拟机加载到内存以后,地址就会跟上边的数字是类似的,根据这个地址信息可以找到命令执行它。

3、特点
(1)线程私有
Jva程序是支持多线程运行的,比如,刚才例子中的那些指令在线程1里运行,同时可能还运行着其他的线程2 线程3 等等,在这种多个线程运行的时候,CPU会由一个调度器组件,给他们分配时间片,比如说给线程1分配一个时间片,若在时间片内,线程1的代码没有执行完,他就会把线程1的状态执行暂存,切换到线程2 中去。因为不能让线程2一直等着,等线程2 代码执行到一定程序,线程2 的时间片用完了,那么切换回来在继续执行线程1的剩余部分代码,这是时间片的概念。那如果在线程切换的过程中,如果我要记住下一条指令执行到哪里了,纳闷还是要用到程序计数器,比如线程1执行到了第九行代码,恰好这个时候他的时间片用完了,CPU切换到线程2执行,这个时候他就会把下一条指令也就是第十行指令的地址记录到程序计数器里面,而且这个程序计数器是属于线程1的,等线程2的代码执行完以后,线程1再次抢到了时间片,线程1就会在自己的程序计数器里把下一行的地址取出来,继续向下运行指令。每个线程都有自己的程序计数器,因为他们各自执行的指令地址都是不一样的,所以每个线程都应该有自己的程序计数器。在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致;
(2)程序计数器是在java虚拟机规范中规定的唯一一个不会存在内存溢出(OOM,OutOfMemoryError)的区;
(3)执行java方法时,程序计数器时有值的,执行native本地方法时,程序计数器的值为空。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JMVM指令地址;或者,如果是在执行native方法,则时未指定值(undefined)。
(4)程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。也是运行速度最快的存储区域;
(5)它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成;
(6)字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

虚拟机栈(Java栈)
学习参考链接:https://blog.csdn.net/PurineKing/article/details/124739693
栈:先进后出
1、概念
虚拟机栈英文全程是java virtual machine stacks。比如,Java中每个线程运行的时候,需要给每个线程划分一个内存空间。虚拟机栈其实是线程运行时需要的内存空间。一个线程,就需要一个栈,如果是多个线程,就需要多个虚拟机栈。

2、栈内由什么组成
一个栈内,可以看做是由多个栈帧组成,比如,一些元素要放入栈的话,这些元素都成之为栈帧。

3、什么是栈帧
一个栈帧对应着一次方法的调用。因为线程最终是为了要执行代码的,这些代码又是由一个个的方法组成的,所以线程运行的时候,每个方法需要的内存称之为一个栈帧。所以栈帧就是每个方法运行时需要的内存。

4、栈帧和栈时怎么联系起来的
比如我调用第一个方法时,他就会给第一个方法分配一个栈帧空间,并且把他压入栈内。当这个方法执行完了,就会把对应的栈帧出栈,就是释放这个方法所占用的内存。一个栈内是可以由多个栈帧的,比如调用方法1,那么这个栈帧就压入到栈里,然后这个方法1又调用了方法2,那么方法2也会产生一个新的栈帧,然后压入到栈中,方法2假设又调用了方法3,那么就会让方法3也会产生栈帧内存,放入栈内。等方法3调用结束,就会把栈帧3的内存释放掉,然后回到方法2,方法2调用结束后,他就会把方法2占用的栈帧内存释放掉并出栈,最后方法1执行完毕后把方法1占用的栈帧内存释放并出栈。

5、整理为三句
(1)每个线程运行时所需要的内存,称为虚拟机栈。
(2)每个栈由多个栈帧(frame)组成,对应着每次方法调用所占用的内存;
(3)每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。活动栈帧就是在栈的顶部的正在执行的方法。

本地方法栈
java虚拟机调用一些本地方法时,需要给本地方法提供的内存空间。本地方法(native method)是指不是由Java编写的的代码,因为Java代码是有一定的限制的,他有的时候不能够去直接跟操作系统底层打交道,所以就需要一些这种用c或c++语言编写的本地方法来真正与操作系统底层的API 打交道,所以Java代码可以间接的去通过本地方法来调用到底层的一些功能,这些本地方法运行的时候他所使用的内存就是本地方法栈。

Java堆
1、定义
像程序计数器、本地方法栈、虚拟机栈都是线程私有的。而堆(Heap)、方法区都是线程共享的区域。通过new关键字创建的对象都会使用堆内存。

2、特点
(1)它是线程共享的,堆中对象都需要考虑线程安全的问题;
(2)有垃圾回收机制,即队中不再被引用的对象就会当成垃圾进行回收,已释放空间的内存,这样不至于堆被创建的对象给撑爆。

3、堆内存溢出
问:堆内存有垃圾回收器,为何还会存在堆内存溢出的问题?
答:对象被当作垃圾回收的条件是这个对象没人再使用他,但是如果不断产生对象,而产生的这些新对象仍然有人再使用他呢?是不是意味着这些对象不能作为垃圾呢,这样达到一定数量之后,堆内存可能就会被耗尽,所以会堆内存溢出。

方法区
学习参考链接:https://blog.csdn.net/qq_33000453/article/details/124923684

1、概念
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区具体包含内容为:
(1)类型信息
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM方法区中存储以下类型信息:这个类型的完整有效名称(全名=包名.类名)、这个类型直接父类的完整有效名(对于interface或者java.lang.Obhect,都没有父类)、这个类型的修饰(public、abstract、final的某个子集)、这个类型直接接口的一个有序列表。

(2)域(Field)信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient某个子集)。

(3)方法(method)信息JVM必须保存所有方法的一下信息,同域信息一样包括声明顺序:
方法名称、方法的返回类型、方法参数的数量和类型(按顺序)、方法的修饰符(punlic ,private,protected,static,final,synchronized,native,abstract的一个子集)、方法的字节码(byteccodes)、操作数栈、局部变量表及大小(abstract和native方法除外)、异常表(abstract 和native方法除外)、每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。

non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即时没有类实例你也可以访问它。全局常量:static final被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

2、方法区溢出
报错:Exception in thread XXX java.lang.OutOfMemoryError Create breakpoint :MetaSpace
Metaspace 区域位于堆外,所以它的最大内存大小取决于系统内存,而不是堆大小,我们可以指定 MaxMetaspaceSize 参数来限定它的最大内存。

二、深拷贝和浅拷贝
浅拷贝:主要是对指针的拷贝,拷贝后两个指针指向同一个内存空间;
深拷贝:需要不但对指针进行拷贝,并对指针指向的内容进行拷贝,经过深拷贝后的指针是指向两个不同的地址的指针。

浅复制:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
深复制:把引用对象的变量指向复制过的新对象,而不是原有的被引用对象。

三、堆栈的区别
物理地址
堆:物理地址分配对象是不连续的,因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。
栈:使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。

内存分别
堆:因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈;
栈:是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

存放的内容
堆:存放的是对象的实例和数组。因此该区更关注的是数据的存储;
栈:存放局部变量、操作数栈、返回结果。该区更关注的是程序方法的执行。(这个不是方法区存储的内容吗?????

PS:
(1)静态变量放在方法区;(2)静态对象放在堆。

程序的可见度
堆:对于整个应用程序都是共享的、可见的;
栈:对于线程是可见的。所以也是线程私有的。他的生命周期和线程相同。

四、队列和栈
队列和栈都是被用来预存数据的。
(1)操作的名称不同。队列的插入称为入队,队列的删除成为出队。栈的插入称为进栈,栈的删除称为出栈。
(2)可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
(3)操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。信赖的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。

五、HotSpot虚拟机对象探秘
HotSpot:一个Java虚拟机(JVM)是从未被物理建成硬件假想计算机。JVM运行编译成其虚构指令集的程序,该指令集作为称为字节码的中间表示写入存储。在运行时,字节码必须从虚构的指令集转换为主机CPU的实际指令集。这可以由口译员即时完成。或者字节码可以被完全编译和缓存,以此通过解释器运行的更快,在一个称为即时(JIT)编译的过程中。HotSpot是JIT技术的一种实现,它从运行解释开始,并观察应用程序的实际性能。然后选择应用程序的某些部分作为本机代码完全编译并缓存,以便更快及执行。

对象的创建
1、创建流程
在这里插入图片描述
1)虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。
2)类加载通过后,接下来分配内存。若java堆中内存是绝对规整的,使用“指针碰撞”方式分配内存;如果不是规整的,就从空闲列表中分配,叫做“空闲列表”方式。划分内存时还需要考虑一个问题-并发,也有两种方式:CAS同步处理;本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)。
3)内存分配完成后,虚拟机需要将分配到的内存空间都进行初始化(即给一些默认值),这样做是为了保证对象实例的字段在Java代码中可以在不赋初值的情况下使用。程序可以访问到这些字段对象所有数据类型的默认值。
4)初始化完成后,虚拟机对对象进行一些简单设置,如标记该对象是哪个类的实力,这个对象的hash码,该对象所处的年龄段等等(这些可以理解为对象实例的基本信息)。这些信息被卸载对象头中。jvm根据当前的运行状态,会给出不同的设置方式。
5)最后执行由开发人员编写的对象初始化方法,把对象按照开发人员的设计进行初始化,一个对象便创建出来了。

2、为对象分配内存。
内存分配根据java堆是否规整,有两种方式,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
*指针碰撞:*如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的放在另一边。分配内存时将位于中间的指针指示器向空间内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
*空闲列表:*如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

JVM 解决分配内存并发问题的方法:
CAS+失败重试方法:保证更新操作的原子性来对分配内存空间的动作进行同步处理。
TLAB:本地线程分配缓冲。把内存分配的动作按照线程划分在不同的空间之中,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM默认会开启-XX:+/-UseTLAB
),-XX:TLABSize指定TLAB大小。
3、处理并发安全问题
在这里插入图片描述
4、对象的访问定位
java程序需要通过JVM栈上的引用方位堆中的具体对象。对象的访问方式取决于JVM虚拟机的实现。目前主流的访问方式有句柄和直接指针两种方式。

(1)句柄访问
句柄,可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如图:
在这里插入图片描述
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的 实力数据指针,而引用本身不需要修改。

直接指针
指针:指向对象,代表一个对象在内存中的起始地址。如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
在这里插入图片描述
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot中采用的就是这种方式。

六、内存溢出异常

java会存在内存泄漏吗

   内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
   但是,即使这样,Java也还是存在着内存泄漏的情况,Java导致内存泄漏的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄漏的发生场景。

七、垃圾收集器

简述java垃圾回收机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,他是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将他们添加到要回收的集合中,进行回收。

GC是什么,为什么要GC
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,java语言没有提供释放已分配内存的显示操作方法。

垃圾回收器的基本原理

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。

垃圾回收器可以马上回收内存嘛

通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是“可达的”,那些对象是“不可达的”。当GC确定一些对象为“不可达”时,GC就有责任回收这些内存空间。

有什么办法主动通知虚拟机进行垃圾回收
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

怎么判断对象是否可以被回收
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的。哪些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要没回收。

一般有两种方法来判断:
引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器+1,引用被释放时计数-1,当计数器为0时就可以被回收。缺点:不能解决循环引用的问题。
可达性分析算法:从GC Root开始向下搜索,搜索所走过的路径称为引用涟。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。

在java中,对象什么时候可以被垃圾回收
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会出发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的/这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

JVM垃圾回收算法
学习链接https://zhuanlan.zhihu.com/p/25539690

GC ROOT的对象有
(1)虚拟机栈中引用的对象(本地变量表)
(2)方法区中静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈中引用的对象(native对象)

1、根搜索算法
根搜索算法是从离散数学中的图论引入的,程序把所有引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
在这里插入图片描述
上图红色为无用的节点,可以被回收。

2、标记-清除算法
在这里插入图片描述
标记-清除算法算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。

3、复制算法
在这里插入图片描述
复制算法将内存划分为两个区间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空间区间)则是空闲的。

复制算法采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。

复制算法在存活对象比较少的时候,极为高效,不会产生内存碎片,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服50%内存的浪费。

4、标记-整理算法
在这里插入图片描述
标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

5、分代收集算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块,一般包括年轻代、老年代和永久代。对于新生代内存的回收(Minor GC)主要采用复制算法。而对于老年代(对象存活率较高)的回收(Major GC),大多采用标记-整理算法。

分代回收器有两个分区:老生代(老生代 + 持久生代)和新生代,新生代默认的空间占比总空间的1/3,老生代的默认占比是2/3.

新生代使用的是复制算法,新生代有3个分区:Eden、To Survivor、From Survivor,他们的默认占比8:1;1,它的执行流程如下:
(1)把Eden + From Survivor 存活的对象放入To Survivor区;
(2)清空Eden 和 From Survivor分区;
(3)From Survivor 和 To Survivor分区交换,From Survivor变To Survivor,To Survivor变From Survivor。

每次在From Survivor到To Survivor移动时都存活的对象,年龄就+1,当年龄到达15(默认配置15)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
在这里插入图片描述
JVM垃圾回收器

参考学习链接:https://zhuanlan.zhihu.com/p/25539690
垃圾收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。下图展示了7种作用于不同分代的收集器。
回收新生代的收集器:Serial、PraNew、Parallel Scavenge
回收老年代的收集器:Serial Old、Parallel Old、CMS
回收整个Java堆收集器:G1
在这里插入图片描述
1、Serial(-XX:+UseSerialGC) 回收新生代收集器
串行回收器。Serial收集器是Java虚拟机中最基本、历史最悠久的收集器。在JDK1.3 之前是Java虚拟机新生代收集器的唯一选择。目前也是ClientVM下ServerVM4核4GB一下机器默认垃圾回收器。Serial收集器并不是只能使用有一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需要暂停所有的用户线程,直接回收结束。

使用算法:复制算法。
在这里插入图片描述
JVM中文名称为Java虚拟机,因此它像一台虚拟的电脑在工作,而其中的每一个线程都被认为是JVM的一个处理器,因此图中的CPU0、CPU1实际上为用户的线程,而不是真正的机器CPU,不要误解哦。

Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。

2、SerialOld(-XX:+UseSerialGC)回收老年代收集器
SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld讲作为后备收集器。

使用算法:标记-整理算法

运行示意图与Serial一致
在这里插入图片描述
3、ParNew(-XX:+UseParNewGC)回收新生代收集器
ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工作。

使用算法:复制算法
在这里插入图片描述
ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器,所以一定要注意使用场景。

4、ParallelScavenge(-XX:+UseParallelGC)回收新生代收集器
ParallelScavenge又被称为吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。

使用算法:复制算法

ParallelCcavenge收集器的目标是达到一个可控件的吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99%。

5、ParallelOld(-XX:+UseParallelOldGC)回收老年代收集器
ParallelOld是并行收集器,和SerialOld一样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6之后才开始提供的,在此之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实。

使用算法:标记-整理算法

在这里插入图片描述
在注重吞吐量与CPU数量大于1的情况下,都可以优先考虑ParallelScavenge + ParallelOld收集器。

6、CMS(-XX:+UseConcMarkSweepGC)回收老年代收集器
CMS是一个老年代收集器,全程Concurrent Low Pasue Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.5 1.6中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用CMS非常合适。

CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。

使用算法:标记-清理

CMS的执行流程:
(1)初始标记(STW initial mark)
在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World).这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。

(2)并发标记(Concreent marking)
这个阶段紧随初始标记阶段,在”初始标记“的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程。

(3)并发预清理(Concreent precleaning)
这个阶段仍然是并发的,JVM查找正在执行”并发标记“阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段”重新标记“的工作,因为下一个阶段会STW。

(4)重新标记(STW remark)
这个阶段会再次暂停正在执行的应用线程,重新从根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比”初始标记“更长,并且这个阶段可以并行标记。

(5)并发清理(Concreent sweeping)
这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。

(6)并发重置(Concreent reset)
这个阶段仍然是并发的,重置CMS 收集器的数据结构,等待下一次垃圾回收。

CMS的缺点:
(1)内存碎片。由于使用了标记-清理算法,导致内存空间中会产生内存碎片。不过CMS 收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是,内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。

(2)需要更多的CPU 资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。

(3)需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过XX:CMSinitiatingOccupancyFraction=n 来设备这个阈值。

7、GarbageFirst(G1)
这是一个新的垃圾回收器,即可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了这个回收器,通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间。悲剧的是,Oracle收购这个收集器之后将其用于商用收费版收集器。因此目前暂时没有发现哪个公司使用它,这个放在之后再去研究。

补充:使用 java -XX:+PrintCommandLineFlags -version 查看JVM采用的GC算法

java -XX:+PrintCommandLineFlags -version

在这里插入图片描述
八、类加载(先不学了)

整篇 参考学习2022最新Android中高级面试题合集.pdf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值