文章目录

jvm是什么?

JVM是Java Virtual Machine(Java虚拟机)的缩写,有着一套虚拟的完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统,JVM是一种用于计算设备的规范,定义了.class文件在其内部运行的相关标准和规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。—百度百科
换成自己的话说,任何语言只要能编译成java虚拟机上运行的目标代码(字节码,也就是.class文件),它就可以在jvm上运行,这个时候我们可以把jvm理解成一个解释器

必要性?

Java语言的一个非常重要的特点就是与平台的无关性即"一次编译,到处运行"。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

种类?

jvm种类有很多,比如HotSpot虚拟机,它是Sun/OracleJDK和OpenJDK中的默认jvm,也是目前使用范围最广的jvm
jvm其实泛指的是HotSpot虚拟机,还有曾经与HotSpot齐名为“三大商业jvm”的JRockit和IBM J9虚拟机

jvm内存布局?

概念

堆(java heap)也叫java堆或者是GC堆,它是一个线程共享的内存区域,也是jvm中占用内存最大的一块区域

存储

java中所有的对象都存储在这里
《Java虚拟机规范》对java堆的描述是:所有的对象实例以及数组都应当在堆上分配

方法区

概念

方法区(Method Area)也被称为非堆区,用于和“java堆”的概念进行区分,它也是线程共享的区域

存储

用于存储已经被jvm加载的类型信息,常量,静态变量,代码缓存等数据

“永久代”是HotSpot中特有的一个概念(Java虚拟机规范中并没有规定这样一个区域),HotSpot技术团队只是用永久代来实现方法区,但这会导致一个致命的问题,这样设计更容易造成内存溢出,因为永久带有-XX:MaxPermSize(方法区分配的最大内存)的上限,即使不设置也会有默认的大小 。例如,32位操作系统中的4GB内存限制等,并且这样设计导致了部分的方法在不同类型的java虚拟机下的表现也不同,比如String::intern()方法
在jdk1.7时,HotSpot虚拟机已经把原来放永久代的字符串常量池和静态变量等移出了方法区
jdk1.8中完全废除了永久代的概念

程序计数器

概念

程序计数器(Program Counter Register)线程独有一块很小的内存区域,他是线程独享的,它记录每个线程当前执行的指令信息(eg:当前执行到哪一条指令,下一条该取哪条指令)
在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线被切换回来的时候能够知道该线程上次运⾏到哪⼉了。

存储

保存当前线程所执行字节码的位置,包括正在执行的指令,跳转,分支,循环,异常处理等

虚拟机栈

虚拟机栈也叫java虚拟机栈(java virtual machine stack)和程序计数器相同它也是线程独享的
用来描述java方法的执行,在每个方法被执行时就会同步创建一个栈帧
用来存储局部变量表,操作栈,动态链接,方法出口等信息
当调用方法时执行入栈,而方法返回时执行出栈

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈类似,他是线程独享的,并且作用也和虚拟机栈类似
本地方法栈指的是调用操作系统原生本地方法(native方法)时所需要的内存区域
只不过虚拟机栈是为虚拟机中执行的java方法服务的,本地方法栈则是为虚拟机使用到的本地方法服务

注意

java虚拟机规范只规定了有这么几个区域,但没有规定jvm的实现细节,因此对于不同的jvm来说,实现也是不同的
例如,“永久代”是HotSpot中的一个概念,而对于JRockit来说就没有这个概念,所以很多人说的JDK1.8把永久代转移到了元空间,这其实只是HotSpot的实现而非《java虚拟机规范》的规定

类加载器

类加载器的作用

顾名思义,当然是加在类文件的呀!我们在idea里面编写的是源代码.java文件,经过javac被编译成.class字节码文件,经过类加载器负责把这些class文件加载到jvm中去执行
Image.png

类加载器分类

1.虚拟机自带的加载器
2.启动类(根)加载器 Bootstrap classLoader

主要负责加载JRE的核心的类库(java.lang.*等,lib目录下的rt.jar,charsets.jar等),构造ExtClassLoader和APPClassLoader。

3.扩展类加载器 ExtClassLoader

主要负责加载jre/lib/ext目录下的一些扩展的jar。

4.应用程序(系统类)加载器 AppClassLoader

主要负责加载应用程序的主函数类。

5.用户自定义类加载器 UserClassLoader

负责加载用户自定义路径下的类包。

~~~什么时候会用上自定义类加载器?

加密:众所周知,java代码很容易被反编译,如果你需要把自己的代码进行加密,可以先将编译后的代码用某种加密算法加密,然后实现自己的类加载器,负责将这段加密后的代码还原。

从非标准的来源加载代码:例如你的部分字节码是放在数据库中甚至是网络上的,就可以自己写个类加载器,从指定的来源加载类。

动态创建:为了性能等等可能的理由,根据实际情况动态创建代码并执行。

~~~典型的自定义类加载器

目前常用web服务器中都定义了自己的类加载器,用于加载web应用指定目录下的类库(jar或class),如:Weblogic、Jboss、tomcat等

双亲委派机制

先来张图

Image.png
图片来源:https://blog.csdn.net/codeyanbao/article/details/82875064(墙裂推荐!!!)

详解双亲委派过程

当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需要的加载的class),子类加载器才会尝试自己去加载。

好处

这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

类加载的几个阶段

其中验证、准备、解析三个阶段称之为连接阶段。
我们通常所说的类加载的五个阶段是加载,验证,准备 ,解析,初始化
image.png

加载阶段(Loading)

主要任务

一、通过一个类的全限定名来获取定义此类的二进制字节流。
二、将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构(这里可以理解为字节流的格式是虚拟机规范规定的,而每个虚拟机中对Java类的数据结构有自己的规定,这一步将外部的二进制字节流转换为虚拟机需要的格式存储在方法区之中)。
三、在内存中生成一个能代表这个类的java.lang.Class对象,作为其他数据访问的入口

可控阶段

这三件事儿中的第一个"通过一个类的全限定名来获取定义该类的二进制字节流"是开发人员可控性最强的,因为虚拟机规范并没有规定一定要从Class文件中获取,所以可以通过定义自己的类加载器来完成(通过重写一个类加载器的loadClass()方法),可以实现从jar、zip、war等压缩包中读取,也可以同网络中获取,甚至可以在运行时计算生成。

注意

加载阶段和连接阶段有部分动作有可能是交叉执行的,比如一部分字节码文件格式的验证,在加载阶段还未完成时就已经开始验证了

验证阶段(Verification)

验证是连接阶段的第一步,这一步的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机自身的安全。此步骤主要是为了验证字节码的安全性(如果纯粹是从Java源码编译得到的Class文件,自身是可以确保安全的,但是因为Class文件可以由任何途径产生)。如果不做安全校验的话,可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是jvm自我保护的一项重要举措

主要任务

image.png

准备阶段(Preparation)

此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上
HotSpot虚拟机在JDK1.7之前都在方法区,而JDK1.8之后此变量会随着类对象一起存放到java堆中

解析阶段(Resolution)

主要任务

此阶段主要是用来解析类,接口,字段及方法的,解析时会把符号引用替换成直接引用

符号引用和直接引用区别

符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
符号引用和直接引用的区别:使用符号引用时,被引用的目标不一定已经加载到内存中;使用直接引用是时,引用的目标必定已经存在虚拟机的内存中了

初始化阶段(Initialization)

初始化是类加载过程的最后一步,初始化阶段就正式开始执行类中编写的java业务代码了,这一步骤之后,类的加载过程就算正式完成了

使用阶段(Using)

卸载阶段(Unloading)

内存泄漏与内存溢出

内存泄漏

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏—例子

申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 521M 的内存一直不回收,那么可以用的内存只有 521M 了,仿佛泄露掉了一部分;
通俗一点讲的话,内存泄漏就是【占着茅坑不拉shi】。

内存泄漏—分类

  • 经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;
  • 偶然发生:在某些特定情况下才会发生;
  • 一次性:发生内存泄露的方法只会执行一次;
  • 隐式泄露:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄露,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。

内存泄漏—常见原因

  1. 循环过多或死循环,产生大量对象;
  2. 静态集合类引起内存泄漏,因为静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放;
  3. 单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
  4. 数据连接、IO、Socket连接等等,它们必须显示释放(用代码 close 掉),否则不会被 GC 回收。
  5. 内部类的对象被长期持有,那么内部类对象所属的外部类对象也不会被回收。

预防方法—静态内部类,弱引用
(在Java中,虽然不需要程序员手动去管理对象的生命周期,但是如果希望某些对象具备一定的生命周期的话(比如内存不足时JVM就会自动回收某些对象从而避免OutOfMemory的错误)就需要用到软引用和弱引用了。
从Java SE2开始,就提供了四种类型的引用:强引用、软引用、弱引用和虚引用。Java中提供这四种引用类型主要有两个目的:第一是可以让程序员通过代码的方式决定某些对象的生命周期;第二是有利于JVM进行垃圾回收。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。)
tips:
强引用: 强引用就是指在程序代码之中普遍存在的。 只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
软引用:软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
虚引用:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

  1. 内存中加载数据量过大;

内存溢出

内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免

内存溢出—例子

申请内存时,没有足够的内存可以使用;
通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。

内存泄漏&内存溢出

image.png
对象 X 引用对象 Y,X 的生命周期比 Y 的生命周期长;
那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;
如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。可见,内存泄漏和内存溢出的关系:内存泄露的增多,最终会导致内存溢出。

解决方案

对于程序崩溃,最快的解决方式是生成dump文件,通过生成dump文件使用调试工具进行调试,还原程序崩溃时的状态,能够起到快速定位排查问题的作用。Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。Dump文件是用来给驱动程序编写人员调试驱动程序用的,这种文件必须用专用工具软件打开,比如使用WinDbg、VS打开。
1、生成dump文件命令:

jmap -dump:format=b,file=fileName.dump pid

2、dump导出到本地,通过专用软件打开,进行分析

GC(Garbage Collection)

如何判断一个对象是否死亡?

引用计数器算法(ReferenceCount)

概念

在创建对象是关联一个与之相对应的计数器
当此对象被使用时加1,销毁时-1
当此计数器为0时,则表示此对象未使用,可以被垃圾收集器回收

优点

垃圾回收比较及时,实时性比较高
只要对象计数器为0,则可以直接进行回收操作

缺点

无法解决循环引用的问题

可达性分析算法(RootSearching)

概念

可达性分析系算法(Reachability Analysis)是目前商业系统中才用的判断对象死亡的常用算法。
他是指从对象的起点(GC Roots)开始向下搜索,如果对象到GC Roots没有任何引用链相连时,也就是说此对象到GC Roots不可达时则表示此对象可以被垃圾回收期所回收.
当时用可达性分析判断一个对象不可达时,并不会直接标识这个对象为死亡状态,而是先将它标记为“待死亡”状态再一次进行校验。校验内容是此对象是否重写了finalize()方法,如果对象重写了finalize()方法(所有对象的finalize方法只会执行一次),那么这个对象将会被存到F-Queue队列中,等待jvm的Finalizer线程去执行重写的finalize()方法,如果此对象将自己赋值给某个变量时,则表示此对象已经被引用了,因此不能被标识为死亡状态,其他情况会被标识为死亡状态

可作为GC Roots的对象

过程示意

常见的GC算法

标记-清除算法(Mark-Sweep)

特点

最早的垃圾回收算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。

优劣

标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片(可能造成没有连续的内存空间)。

回收过程

标记-复制算法(Copying)

特点

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题,是标记-清除算法的一个升级。它开始时把堆分成 一个对象面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

优劣

克服了句柄的开销和解决内存碎片的问题
导致可用的内存变为原来的一半,导致了内存的可用率大幅降低

回收过程

标记-整理算法(Mark-compact)

概念

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针**。**

优劣

标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

回收过程

分代收集算法

概念

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

分代模型

分代回收
~~~年轻代(Young Generation)的回收算法 (回收主要以Copying为主)

a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
b) 新生代内存按照8:1:1的比例分为一个eden(伊甸园)区和两个survivor(幸存者)(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

~~~年老代(Old Generation)的回收算法(回收主要以Mark-Compact为主)

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

~~~持久代(Permanent Generation)的回收算法

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区。

~~~~~~JDK1.7和JDK1.8内存模型有什么区别?问什么取消了永久代?

jkd1.7内存模型

jkd1.8内存模型

为什么取消永久代?
1.移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
Oracle有两个JVM,一个是JRockit, 这是之前前收购BEA Systems时得到的;另一个则是Sun的Hotspot VM,这是收购Sun时得到的。在举行的Sun-Oracle未来路线图会议上,Oracle的管理团队表示要合并这两个项目。
2.由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen

常见的垃圾回收器

Serial

概念

最早的垃圾收集器,也是jdk1.3版本以前唯一的垃圾收集器
它是单线程垃圾收集器,单线程是指在进行垃圾回收时所有的工作线程必须暂停直到垃圾回收结束为止

图示

image.png

特点

简单和高效,并且本身的运行对内存的要求不高,因此它在客户端模式下使用比较多

ParNew收集器

概念

实际上是Serial收集器的多线程并行版本

图示

image.png

Parallel Scavenge

概念

Parallel Scavenge收集器和ParNew收集器类似,也是并行运行的垃圾收集器

图示

image.png

特点

与ParNew收集器不同:Parallel Scavenge收集器关注的侧重点–实现一个可以控制的吞吐量
计算公式:用户运行代码的时间/(用户运行代码的时间+垃圾回收执行的时间)

目标

Parallel Scavenge收集器的目标是将吞吐量控制在一定范围内

参数

1.-XX:MaxGCPauseMillis参数:用来控制垃圾回收的最大停顿时间
2.-XX:GCTimeRatio参数:用来直接设置吞吐量值的

Serial Old收集器

为Serial收集器的老年代版本

Parallel Old收集器

Parallel Scavenge收集器的老年代版本

CMS(Concurrent Mark Sweep)收集器

特点

CMS收集器与以吞吐量为目标的Parallel Scavenge收集器不同,它强调的是提供最短的停顿时间,因此可能会牺牲一定的吞吐量。主要应用在Java Web项目中,满足了系统需要短时间停顿的要求。

实现过程

~~~初始标记

~~~并发标记

~~~重新标记

~~~~~~remark阶段的算法—三色扫描算法

扫描中:

灰色表示正在扫描的节点上,自己已经标记完成,还没来得及标记fields

黑色表示已经被垃圾回收器访问过的,自己已经标记完成,fields也已经标记完成

白色表示没有被垃圾回收器访问到的节点,如果扫描结束之后,还是白色的,代表不可达,可以回收。

扫描结束:

黑色节点代表存活对象

白色代表代表可回收对象

存在问题–漏标:

在这里插入图片描述
从图1到图2,最开始,A已经处理完了,B本身已经处理完了,它的孩子(D)还没有来得及处理(图1)
垃圾回收线程和业务逻辑线程交叉(并发)执行
后来,A有个引用指向D了,B指向D的引用消失了(图2)
存在问题:
A.d=D,A有个引用指向了D,站在垃圾回收器的角度,不会再扫D,因为A是黑色的,表示已经处理完了,不会再去扫它的孩子
B.d=null,B指向D的引用就断了,站在垃圾回收器的角度,B就找不到D了
此时,A不会去找D,B找不到D,D会被我们当成垃圾清掉,此时再执行A.d就会报出空指针异常

如何解决呢?
在运行过程当中,如果发现一个黑色的对象的成员变量又指向了一个白色对象,那么就把它的黑色变成灰色
在这里插入图片描述
CMS缺陷:虽然把A变成灰色了,依然会产生漏标

有一个垃圾回收线程正在标记A,它的第一个孩子1已经标记完成,它的第二个孩子2还没有标完,此时,因为A还没有标记完所有的孩子,所以A是灰色;此时,A的属性1指向一个白色对象,根据上面的解决方案,在运行过程当中,如果我发现一个黑色的对象的成员变量又指向了一个白色对象,那么就把它的黑色变成灰色,但是此时A本来就是灰色,因此不需要改变;接下来,A的第二个孩子2标记完成,此时,A标记完了所有的孩子,因此A变为黑色,变为黑色的A不需要再扫描它的孩子,因此,D漏标
在这里插入图片描述

~~~并发清除

需要stop the world的阶段

后续问题

CMS是一款基于标记清除算法实现的垃圾收集器,因此会在收集时产生大量的空间碎片
为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection的参数(默认是开启的,此参数从JDK9开始废弃)用于在CMS收集器进行Full GC时开启内存碎片的合并和整理,因为碎片整理的过程必须移动存活的对象,所以它和用户线程是无法并发执行的,为了解决这个问题CMS收集器又提供了另外一个参数-XX:CMSFullGCsBefore-Compaction用于规定多少次(根据此参数的值决定)之后再进行一次碎片整理

Garbage First(简称G1)收集器

特点

是历史发展的产物,也是一款更先进的垃圾收集器,主要面向服务端应用的垃圾收集器
1.区域块级化,增加 H 区 。(大对象,巨型对象区)
2.优化垃圾回收清理阶段的逻辑。
3.由用户控制最大 STW 时间,优化用户体验。

g1垃圾回收模型

实现过程

G1 的垃圾回收过程为:初始标记 -> 并发标记 -> 最终标记 -> 筛选回收

较CMS比较
~~~筛选回收

其中最重要的改变是筛选回收过程变为 STW(stop the world)
为什么叫做筛选回收呢?它维护一个优先级列表,根据用户设置的最大 STW 时间,选取回收收益最大的Region 区域进行回收,也就是可能每次的垃圾回收是不一定能完全回收掉所有垃圾的。

~~~区域块级化

同时因为区域块级化,G1 在回收的时候会采用标记复制的算法进行回收(年轻代回收和老年代回收都是) 这样大大的提高了回收的效率,同时又因为它的块级区即 Region 区是不固定死年代划分的,所以整体看来相当于标记整理算法,没有内存碎片。这是相对于 CMS 标记清除的一大优化。G1将堆划分为2048个region(大小为1~32M,2的幂次方),每个region从属不同的年代(注意:region并不固定属于某个年代,有时候属于young,有时候属于old,根据其保存对象来决定),每个年代都是一部分region的集合。

ZGC

特点

1、ZGC收集器是JDK11中新增的垃圾收集器,它是由Oracle官方开发的,并且支持TB级别的堆内存管理,而且ZGC收集器也非常高效,可以做到10ms以内完成垃圾收集
2、在ZGC收集器中没有新生代和老生代的概念,它只有一代
3、ZGC收集器采用的着色指针技术,利用指针中多余的信息位来实现着色标记,并且ZGC使用了读屏障来解决GC线程和应用线程可能存在的并发(修改对象状态的)问题,从而避免了Stop The World(全局停顿),因此使得GC性能大幅提升

较CMS比较

ZGC的执行流程和CMS比较相似,首先是进行GC Roots标记,然后再通过指针进行并发着色标记,之后便是对标记为死亡的对象进行回收(被标记为橘色的对象),最后是重定位,将GC之后存活的对象进行移动,以解决内存碎片的问题

C4

Azul System公司的C4(Concurrent Continuously Compacting Collector 并行连续压缩收集器,译者注,Azul官网给出的名字是Continuously Concurrent Compacting Collector)算法使用独一无二而又非常有趣的方法来实现低延迟的分代式垃圾回收。

不同JDK版本主流垃圾收集器

JDK8主流的垃圾收集器是CMS(UseParallelGC 是jdk9之前虚拟机运行正在server模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收;UseParallelGC是使ParallelScavenge+SerialOld收集器组合;UseParallelOldGC是使用ParallelScavenge+ParallelOld收集器组合)
JDK9之后也成了官方默认的垃圾收集器,官方也推荐使用G1来代替选择CMS(Concurrent Mark Sweep)收集器
JDK11中的ZGC(Z Garbage Collector)

总结

以上是小编对于jvm目前阶段学习的总结,还有很多需要深入的地方,比如,各个垃圾回收器的优劣对比,存在的缺陷,新的垃圾回收器通过什么方式解决了以前的问题,,,等等等等,期待下次的分享噻!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐