java虚拟机基础(一)
1. 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机在执行字节码时,把字节码解释成具体平台上的及其指令执行。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
2.java内存区域 ( java7 )
2.1 java虚拟机包含了哪些模块
这张图我们要印象深刻,JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
每个模块的作用:
(1)首先通过编译器把 Java 代码转换成字节码,
(2)类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内。
(3)而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
2.2 java7内存结构(运行时数据区)
首先对这个图有一个认识,从上面可以看到java7的内存结构大致分了五个部分:PC寄存器,java虚拟机栈、本地方法栈、java堆、方法区。其中PC寄存器、java虚拟机栈和本地方法栈是所有线程私有的一块内存区域。java堆和方法区是每一个线程共享的一块区域,其中,方法区还有一个运行时常量池。
接下来看一看每一块区域里面存放的什么?
(1) pc寄存器
内存里面有很多寄存器,大概几百个吧,其中有一个寄存器就是程序计数器。这个寄存器的主要作用就是存放下一条需要执行的指令的地址。
首先,**为什么要有这个程序计数器呢?**这是因为我们的处理器在一个时刻,只能执行一个线程中的指令。但是我们的程序往往都是多线程的,这时候处理器就需要来回切换我们的线程,为了在线程切换之后回到之前正确的位置上,此时就需要一个程序计数器,这也就很容易理解了我们的每个线程都有一个自己的程序计数器来保存自己之前的状态。
如何理解这个程序计数器的功能呢?假如我们的程序代码假如是一行一行执行的,程序计数器永远指向下一行需要执行的字节码指令。在循环结构中,我们就可以改变程序计数器中的值,来指向下一条需要执行的指令。因此,在分支、循环、跳转、异常处理和线程恢复等等一些场景都需要这个程序计数器来完成。
如果当前执行的是 Java 的方法,则该寄存器中保存当前指令的地址;倘若执行的是native 方法,则PC寄存器中为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
因此可以把他的几个特点归纳如下。
- 程序计数器指定下一条需要执行的指令
- 每一个线程独有一个程序计数器
- 执行java代码时,寄存器保存当前指令地址
- 执行native方法时候,寄存器为空。
- 不会造成OutOfMemoryError情况
(2)Java虚拟机栈
每一个线程都有自己的java虚拟机栈,这个栈与线程同时创建,一个线程中的每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、动态连接和返回地址等信息。当前运行方法对应的栈帧叫做当前栈帧。下面主要对这个栈帧进行一个介绍。
局部变量表里存放了编译期间可知的各种基本数据类型(8种)、对象引用、returnAddress类型(指向一条字节码指令的地址)。他有如下特点:
- 64位长度的long和double类型占用2个局部变量空间(Slot),其余数据类型只占用一个。
- 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的
- 在方法运行期间不会改变局部变量表的大小。
操作数栈,其实在栈帧刚刚创建的时候,操作数栈是空的,java虚拟机可以从局部变量表或者对象的实例字段中,复制一些常量或者变量值到操作数栈中。也可以从操作数栈中取走数据。他的深度在编译期就已经确定了。
动态连接,在线程中一个方法去调用另外一个方法,是通过符号引用来实现的,动态连接的作用就是把这个符号引用表示的方法转化为实际方法的直接引用。
对于java虚拟机栈的描述,最后看一下可能发生的异常情况:
- 如果线程请求分配的栈容量超过java虚拟机栈所允许的最大容量,java虚拟机就会抛出StackOverfolwError
- 如果java虚拟机栈动态扩展,在扩展时没有申请到足够的内存或者是创建新线程时没有足够的内存再创建java虚拟机栈了,那么java虚拟机就会抛出outOfMemoryError
(3)本地方法栈
与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。本地方法栈可以抛出StackOverflowError和OutOfMemoryError异常。
(4)Java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用来存放对象实例。是内存中最大的一块区域。垃圾收集器(GC)在该区域回收不使用的对象的内存空间。但是并不是所有的对象都在这保存,随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量调换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上也逐渐变得不那么绝对了。
(5)方法区
方法区也是所有线程共享。主要用于存储类的信息、常量池、静态变量、及时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分。通常又叫“Non-Heap(非堆)”。
2.3 使用例子理解java7内存结构
假设我们定义了3个类:
接下来就开始分析这些代码在运行时内存的变化。现在在我们的IDE开始运行Test类:
(1) 第一步,JVM去方法区寻找Test类的代码信息,如果有直接调用,没有的话使用类的加载机制把类加载进来。同时把静态变量、静态方法、常量加载进来。这里加载的是(“冯冬冬的IT技术栈”,“冯XX”);这是因为字符串是常量,age中的18是基本类型。
(2) 第二步,jvm进入main方法,看到Person person=new Person()。首先分析Person这个类,同样的寻找Person类的代码信息,有就加载,没有的话类加载机制加载进来。同时也加载静态变量、静态方法、常量(“我正在走路。。。”)
(3) 第三步,jvm接下来看到了person,person在main方法内部,因而是局部变量,存放在栈空间中。
(4) 第四步,jvm接下来看到了new Person()。new出的对象(实例),存放在堆空间中。
(5) 第五步,jvm接下来看到了“=”,把new Person的地址告诉person变量,person通过四字节的地址(十六进制),引用该实例。
(6)第六步,jvm看到person.name = “冯冬冬的IT技术栈”;person通过引用new Person实例的name属性,该name属性通过地址指向常量池的"冯冬冬的IT技术栈"。
(7)第七步,jvm看到person.age = 18; person的age属性是基本数据类型,直接赋值。
(8)第八步,jvm看到person.walk(); 调用实例的方法时,并不会在实例对象中生成一个新的方法,而是通过地址指向方法区中类信息的方法。走到这一步再看看图怎么变化的。
(9)第九步,jvm看到Baby baby=new Baby().这个过程和Person person = new Person()一样
(10)第十步,jvm看到baby.babyname = “冯XX”;这个过程也和person.name = “冯冬冬的IT技术栈”;一样。
(11)第十一步,jvm看到person.baby = baby;把baby对象引用赋值给Person实例的baby属性属性。
整个程序的执行流程就是这样。
3. java8内存结构
java8内存结构,那一定是在java7的基础上和其作比较,因此首先解释一下两个名词:永久代(PermGen)和元空间(Metaspace)。
首先是永久代:
我们常见的 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。
然后是元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。而永久代在jvm中,永久代的大小收到JVM的内存限制。
3.1 java8的内存结构图
3.2 java7到java8的第一部分变化
(1)元空间
java7的永久代被替换为元空间。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。而永久代在jvm中,永久代的大小收到JVM的内存限制。
(2)运行时常量池的变化
运行时常量池(RuntimeConstant Pool)的所处区域一直在不断的变化,在java6时它是方法区的一部分;1.7又把他放到了堆内存中;1.8之后出现了元空间,它又回到了方法区。
4. 说一下堆和栈的区别?
(1)物理内存堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
(2)存放的内容堆存放的是对象的实例和数组。因此该区更关注的是数据的存储;栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
(3)程序的可见度堆对于整个应用程序都是共享、可见的。栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
5.一个java对象是如何创建的?
java对象的创建包含了5个步骤:类加载检查、内存分配、初始化零值、设置对象头、执行init方法。
(1)类加载
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中是否已经加载该类。如果没 有,那必须先执行相应的类加载过程。
(2)内存分配
类加载通过后,接下来分配内存。对象所需内存的大小在类
加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
划分内存的方法?
-
“指针碰撞”(Bump the Pointer),如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
-
“空闲列表”(Free List),如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
**选择这两种方法取决于java堆内存是否规整,java堆的是否规整又与GC采用的算法是标记-清楚算法还是标记-整理算法有关。**复制算法的内存也是规整的。
内存分配的时候容易引发并发问题,在并发情况下,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决并发问题的方法?
- CAS(compare and swap)同步处理,虚拟机采用CAS加上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
(3)初始化
内存分配完成后,虚拟机需要堆内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
(4)设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header之中。
(5)执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
6. JVM的垃圾回收机制
6.1 为什么要进行垃圾回收?
我们知道,在平时的开发当中,有时候我们需要创建大量的对象,如果我们动态创建的对象没有得到及时回收,持续堆积,最后会导致内存被占满,造成溢出。因此Java 提供了一种垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把内存的垃圾全部进行回收,从而保证程序的正常运行。垃圾回收机制就是描述的最后一个类的卸载过程,也就是把垃圾如何从内存中回收的问题。
6.2 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
(1)对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
(2)垃圾回收器可以马上回收内存。
(3)程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
6.3 如何确定哪些对象是垃圾呢?
为了确定哪些对象是垃圾,jvm为我们提供了一些算法去判定。常见的判断是否存活有两种方法:引用计数法和可达性分析。
1、引用计数法
为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。每当有一个地方去引用它时候,引用计数器就增加1。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。
2、达性分析
可达性分析基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,开始向下搜索,当一个对象没有一条路径可以到达GC Roots的时候,证明该对象是不可达的,需要被回收。
下面这张图就是可达性分析的描述:
那么,Java 里有哪些对象可以作为GC Roots呢?主要有以下四种:
- 虚拟机栈(帧栈中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI 引用的对象。
6.4 Java 中都有哪些引用类型?
在jdk1.2之前,java对引用的概念只有“已被引用”和"未被引用"两种状态。后来所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用、软引用、弱引用、虚引用4 种,也就是我们今天所讲的主题。这 4 种引用的强度依次减弱。
1、强引用
如果一个对象具有强引用,它就不会被垃圾回收器回收。New 关键字创建的对象的引用就是强引用。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。比如String str = new String(“hello”) 这时候str就是一个强引用。当然我们也可以使用str=null取消一个强引用。下面我们使用代码来测试一下强引用:前提是我们先要设置一些jvm参数, -Xms2M -Xmx3M表示初始内存是2M,最大内存是3M。
2、软引用
如果一个对象具有软引用,它就不会被垃圾回收器回收。只有在内存空间不足时,软引用才会被垃圾回收器回收。这种引用常常被用来实现缓存技术。因为缓存区里面的东西,之后在内存不足的时候才会被清空。下面我们再进行测试一下:前提是设置虚拟机参数 -Xms2M -Xmx3M。
3、弱引用
如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。我们把上面的对象如果改成弱引用的话,你会发现所有的全部为空,这就是因为创建了10个1M的对象,超出了3M,无论内存是否足够,都会被回收。
4、虚引用
如果一个对象具有弱引用,就相当于没有引用,在任何时候都有可能被回收。虚引用是使用PhantomReference创建的引用,虚引用也称为幽灵引用或者幻影引用,是所有引用类型中最弱的一个。一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。
使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。
6.5 有哪些垃圾回收算法
1、标记-清楚
第一步(标记),利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记。第二步(清理),我们再遍历一遍,把所有“垃圾”对象所占的空间直接 清空 即可。
此算法的特点:
- 简单方便
- 容易产生内存碎片
2、标记-整理(标记-压缩)
上面的方法我们发现会产生内存碎片,因此在这个方法中同样为两步:
第一步(标记):利用可达性遍历内存,把“存活”对象和“垃圾”对象进行标记。
第二步(整理):把所有存活对象堆到同一个地方,这样就没有内存碎片了。
此算法特点:
- 活对象多,垃圾少的情况
- 需要整理的过程
3、复制算法
将内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还活着的对象复制到另一块上,然后再把使用过的内存空间一次性清理掉
此算法特点:
- 简单
- 不会产生碎片
- 内存利用率太低,只用了一半
4、分代算法
根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
6.6 堆和方法区的垃圾回收
java中的垃圾回收大致在两部分,第一个就是堆、第二个就是方法区。
1、方法区的垃圾回收
方法区又叫做永久代。永久代的垃圾回收主要有两部分:废弃常量和无用的类。
首先是废弃常量垃圾回收的一般步骤:
第一步:判定一个常量是否是废弃常量:没有任何一个地方对这个常量进行引用就表示是废弃常量。
第二步:垃圾回收
然后是无用的类垃圾回收的一般步骤
第一步:判定一个类是否是“无用的类”:需要满足下面三个条件
- Java堆中不存在该类的任何实例,也就是该类的所有实例都被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象在任何地方没有引用了,也不能通过反射访问该类的方法。
第二步:满足上面三个条件就可以回收了,但不是强制的。
2、Java 堆的垃圾回收:分代回收算法
我们先看一下java堆的结构:
Java 堆空间分成了三部分,新生代、老年代和永久代,这三部分用来存储三类数据:
- 刚刚创建的对象。
- 存活了一段时间的对象。
- 永久存在的对象。
也就是说,常规的 Java 堆至少包括了新生代 和 老年代 两块内存区域,而且这两块区域有很明显的特征:
- 新生代:存活对象少、垃圾多
- 老年代:存活对象多、垃圾少
针对这种特点,我们有如下的几种方案:
(1)新生代-复制回收机制
对于新生代区域,由于每次GC 都会有大量新对象死去,只有少量存活。因此采用 复制 回收算法,GC 时把少量的存活对象复制过去即可。但是从上面我们可以看到,新生代也划分了三个部分比例:Eden:S1:S2=8:1:1。其中 Eden 意为伊甸园,形容有很多新生对象在里面创建;S1和S2中的S表示Survivor,为幸存者,即经历 GC 后仍然存活下来的对象。
工作原理:
- 首先,Eden对外提供堆内存。当 Eden区快要满了,触发垃圾回收机制,把存活对象放入 Survivor A 区,清空 Eden 区;
- Eden区被清空后,继续对外提供堆内存;
- 当 Eden 区再次被填满,对 Eden区和 Survivor A 区同时进行垃圾回收,把存活对象放入 Survivor B区,同时清空 Eden 区和Survivor A 区;
- 每次在垃圾回收后还存活的独享,年龄就 +1,当年龄到达 15(默认配置是15)时,升级为老生代。大对象也会直接进入老生代,大对象是指大小大于Survivor 区域的50%。
- 如果某个 Survivor区被填满,会将所有对象放到老年代;
- 当 Old 区也被填满时,进行 下一阶段的垃圾回收。
(2)老年代-标记整理回收机制
老年代的特点是:存活对象多、垃圾少。因此,根据老年代的特点,这里仅仅通过少量地移动对象就能清理垃圾,而且不存在内存碎片化。也就是标记整理的回收机制。既然是标记整理算法,而且老年代内部也不存在着内存划分,所以只需要根据标记整理的具体步骤进行垃圾回收就好了。
6.7 Minor GC和Major GC?
多数情况,对象都在新生代Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。这里我们提到 MinorGC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现Major GC/Full GC。
(1)Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
(2)Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
7.常见的垃圾回收器
可以参考JVM几种常见的垃圾收集器总结
7.1 垃圾回收时为什么会出现停顿
垃圾收集的一个前提是要判断进程中的对象哪些是垃圾内存,哪些不是。怎么判断呢,JVM里面使用了一种叫可达性分析的技术来枚举根节点。在这个可达性分析过程中,是必须要求分析过程中树结构是不变的,也就是一致的。这意味着这个过程中,当前JAVA进程必须暂停,这就是停顿的根本原因。
7.2 垃圾收集器
常见的垃圾回收器有7种,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
(1)Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
(2)ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
(3)Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
(4)Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
(5)Parallel Old收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
(6)CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
它实现了让垃圾收集线程和用户线程同时工作。其执行的整个过程分为4步:
- 初始标记:暂停所有其他线程,该阶段进行可达性分析,标记GC ROOT能直接关联到的对象。注意是直接关联间接关联的对象在下一阶段标记。
- 并发标记:该阶段进行GC ROOT TRACING,在第一个阶段被暂停的线程重新开始运行。由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。
- 重新标记:暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象。有了前面的基础,这个阶段的工作量被大大减轻,停顿时间因此也会减少。注意这个阶段是多线程的。
- 并发清楚:用户线程被重新激活,同时清理那些无效的对象。
cms收集器的优点:并发收集、低停顿
缺点:
- 对cpu资源敏感
- 无法处理浮动垃圾
- 采用标记-清楚算法会产生大量碎片空间
(7)G1(Garbage First)收集器 (标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。