三、JVM
1.说一下 jvm 的主要组成部分?及其作用?
- 类加载器
加载类文件到内存,并为之创建一个class对象。 - 运行时数据区
JVM的内存分布。
(1)堆
堆是java对象的存储区域,任何new出来的对象实例或者数组都分配在堆上。可以用-Xms或者-Xmx进行内存控制。jdk1.7之后,运行时常量池从方法区移到了堆上。
(2)方法区
用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
误区:方法区不等于永生代
很多人原因把方法区称作“永久代”(Permanent Generation),本质上两者并不等价,只是HotSpot虚拟机垃圾回收器团队把GC分代收集扩展到了方法区,或者说是用来永久代来实现方法区而已,这样能省去专门为方法区编写内存管理的代码,但是在Jdk8也移除了“永久代”,使用Native Memory来实现方法区。
(3)虚拟机栈
在执行每个方法时,虚拟机栈都会创建一个栈帧用于存储局部变量表(存放方法参数和方法内部定义的局部变量)、操作数栈(虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈)、动态链接(栈帧中保存了一个引用可以指向方法所在类的运行时常量池,能够从常量池中找到对应的符号引用,然后将符号引用转化为直接引用,就能找到相应的方法了)、方法出口(当一个方法执行的时候,只有两种可以退出方法的方法。第一种是JVM碰到任意一个方法返回的字节码指令,被称为正常完成出口。另一种是在执行方法中抛出异常并且未对异常进行处理,被称为异常完成出口。方法退出的时候相当于把栈帧出栈。)等信息。
(4)本地方法栈
与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
(5)程序计数器
指示Java虚拟机下一条需要执行的字节码指令。 - 执行引擎
将字节码指令编译为对应平台上的本地机器指令。 - 本地库接口
融合不同的语言为java所用。
2.说一下 jvm 运行时数据区?
(1)程序计数器
- 占据一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。字节码解释器工作时,通过改变程序计数器的值,来确定下一条执行的字节码指令。
- 由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何时刻,处理器都只会执行一条线程中的指令。因此为了保证线程切换后能回到之前的正确位置,每条线程都有自己的程序计数器,各线程之间互不影响。
- 如果正在执行的是java方法,那么程序计数器记录的是正在执行的虚拟机字节码地址。如果执行的是native方法,计数器为空。
(2)Java虚拟机栈 - 线程私有,生命周期和线程相同,Java虚拟机栈描述的是Java方法执行时的内存模型。在每个方法执行时都会创建一个栈帧,用来保存局部变量表、操作数栈、动态链接、方法出口。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表存放了编译期可知的各种基本类型数据(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。
- 其中64位长度的long和double类型的数据在局部变量表中会占用2个slot,其他类型的数据占用一个slot。局部变量表所需的空间在编译期完成分配,当进入一个方法时,这个方法需要多少空间是确定的,在方法运行期间不会改变局部变量表的大小。
- 如果线程请求的栈深度,大于虚拟机所允许的栈深度,会抛出StackOverflow异常;如果虚拟机栈可以动态扩展时,无法申请到足够的内存,会抛出OutOfMemory异常。
(3)本地方法栈
本地方法栈与虚拟机栈所发挥的作用非常相似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机中使用到的native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一,与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。
(4)Java堆
Java堆被线程共享,用来存放对象实例和数组。Java堆是垃圾收集器的主要管理区域。从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
(5)方法区
和堆一样所有线程共享,用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等。
jdk1.7使用永久代来实现方法区,在jdk1.8之后,使用元空间来实现方法区。
永久代:在运行时开辟空间实现方法区。
元空间:在本地内存区域开辟空间实现方法区。
永久代中的数据空间在每次fullgc的时候可能被收集,为永久代分配多少空间很难确定,超出指定空间容易造成内存泄漏。
元空间的特点:
1.类及相关的元数据的生命周期与类加载器的一致
2.每个加载器有专门的存储空间
3.只进行线性分配
4.不会单独回收某个类
5.省掉了GC扫描及压缩的时间
6.元空间里的对象的位置是固定的
7.如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
(6)运行时常量池
- 运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
- Java虚拟机对class文件每一部分的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被jvm认可。但对于运行时常量池,Java虚拟机规范没做任何细节要求。
- 运行时常量池有个重要特性是动态性,Java语言不要求常量一定只在编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也有可能将新的常量放入池中,这种特性使用最多的是String类的intern()方法。
- 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制。当常量池无法再申请到内存时会抛出outOfMemeryError异常。
在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
3.对象创建的过程?
- 检查指令的参数(即工作中我们New的对象),能否在常量池中找到它的符号引用。
- 如果存在,检查符号引用代表的类是否被加载、解析、初始化过。(如果没有则执行类的加载)。
- 加载通过后,虚拟机将为新生对象分配内存。(所需内存大小在类加载完成后便可确定)
4.内存分配的方式?
- 指针碰撞:假设Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边。中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针往空闲空间那边挪动一段与对象大小相等的距离。这种方式则属于指针碰撞。
- 空闲列表:如果堆中的内存并不是规整的,已使用的内存和空闲内存相互交错,显然无法使用指针碰撞。虚拟机就必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新记录表上的数据。这种方式属于空闲列表。
具体选择哪种分配方式由Java堆决定,而Java堆是否规整,则有GC收集器决定。因此使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞。而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用的空闲列表。
5.对象的内存分布?访问定位?
在HotSpot虚拟机中对象的内存布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头包括两部分信息:
存储对象自身的运行时数据(如:哈希码、GC分代年龄、锁 等)
类型指针(即对象指向他的类元数据的指针,虚拟机根据此指针来确认对象属于哪个类的实例)实例数据:
实例数据才是对象真正存贮的有效信息(即程序中所定义的各种类型的字段内容)。对齐填充:
不是必然存在的,仅仅起到占位符的作用。
对象的访问方式:-
句柄访问:Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
优点:reference中存储句柄地址是稳定的。在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
句柄访问图示:
-
指针访问:reference中存储的直接就是对象地址。
优点:速度快,节省了指针定位的时间成本。
指针访问图示:
6.说一下堆栈的区别?
1.栈内存中保存的是局部变量,堆内存中保存的是对象的实例和数组。
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期短。
3.栈内存中保存的变量生命周期结束后会被释放,而堆内存中保存的对象实例和数组会被垃圾回收机制不定期回收。
4.栈是线程独享的,堆是线程共享的。但是TLAB例外。TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。TLAB在读取上确实是线程共享的,但是在内存分配上,是线程独享的。
7.队列和栈是什么?有什么区别?
- 队列:
在表的一端插入,在另一端删除的线性表。
先进先出。
基于地址指针进行遍历,可以从头部或者尾部进行遍历,但是不能同时遍历,无需开辟空间,因为在遍历过程中不影响数据结构,所以遍历的速度快。 - 栈:
只能在表的一端进行插入和删除操作的线性表。
先进后出。
只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,遍历数据时需要微数据开辟临时空间,保持数据在遍历前的一致性。
8.说一下类加载的执行过程?
一个Java文件从编码到执行,一般经历两个过程。编译和运行。编译是指把java文件通过javac命令编译成字节码,也就是.class文件。运行是指把编译生成的.class文件交给JVM去执行。
类加载就是指JVM把.class文件加载到内存中,生成class实例的过程。
类加载分为三个过程:
- 加载
- 链接
- 初始化
而链接又可以分为3个过程: - 验证
- 准备
- 解析
加载
将class字节码文件从各个来源通过类加载器加载到内存中。
字节码来源:本地路径下的文件、jar包、远程网络、动态代理编译等。
类加载器:一般包括启动类加载器、扩展类加载器、应用类加载器、用户的自定义类加载器。
验证
保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
文件格式验证。常量中是否有不被支持的常量?
元数据验证。该类是否继承了被final修饰的类?类中的字段和方法时候与父类冲突?是否有不合理的重载?
字节码验证。保证程序语义的合理性。
符号引用的验证。校验符号引用中是否可以通过全限定名找到对应的类?校验符号引用中的访问权限是否可以被当前类访问?
准备
为类变量(不是实例变量)分配内存,设置初始值。
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值。
解析
将常量池中的符号引用替换为直接引用。
符号引用:一个字符串,但是这个字符串给出了一些能够唯一确定识别一个变量、一个方法、一个类的信息。
直接引用:可以理解为一个内存地址或者一个偏移量。
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,JVM会把所有的常量名、方法名、类名这些符号引用,替换为具体的内存地址或者偏移量,也就是直接引用。
初始化
对类变量进行初始化,是执行类构造器的过程。
只有对static修饰的变量或者语句进行初始化。
如果初始化一个类的时候,其父类未被初始化,则先初始化其父类。
9.什么是双亲委派模型?
类加载器主要分为启动类加载器、扩展类加载器、应用类加载器和自定义类加载器。
当需要加载前一个类的时候,子类加载器并不会马上加载,而是一次去请求父类加载器,最终是到启动类加载器。当启动类加载器加载不了的时候,再依次往下让子类加载器去加载。当到达最底下还是加载不了的时候,就会抛出classnotfound异常。
好处:保证了程序的安全性。例子:比如我们重新写了一个String类,加载的时候并不会去加载到我们自己写的String类,因为当请求上到最高层的时候,启动类加载器发现自己能够加载String类,因此就不会加载到我们自己写的String类了。
有时会违反双亲委派模型的约束,比如jdbc。
Java提供了很多SPI(Service Provider Interface,服务提供者接口),允许第三方为这些接口提供实现。JDBC在获取connection时,调用DriverManager中的方法。但是因为DriverManager在rt.jar里面,它的类加载器时启动类加载器。而数据库的driver(com.mysql.jdbc.Driver)是放在classpath里面的,启动类加载器是不能加载的。所以,如果严格按照双亲委派模型,是没办法解决的。而这里的解决办法是:通过调用类的类加载器去加载。而如果调用类的加载器是null,就设置为线程的上下文类加载器:Thread.currentThread().getContextClassLoader()
9.怎么判断对象是否可以被回收?
1.引用计数法(已被淘汰)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象
之间相互循环引用的问题。尽管该算法执行效率很高。
2.可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”。当一个对象到GC Roots没有任何引用链相连接时,则证明此对象不可用。
Java中可作为GC Roots的对象:
1.虚拟机栈(栈帧中的局部变量表)中引用的对象。
2.方法区中静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
方法区存储内容是否需要回收的判断不一样。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:
- 该类的所有实例已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的class对象已经被回收,无法在任何地方通过反射访问到该类的方法
10.被GC判断为”垃圾”的对象一定会回收吗?
如果对象在进行了可达性分析后发现没有与GC Roots相连的引用链,那么会对这些对象进行第一次标记并且做一次筛选。筛选的条件是这些对象是否有必要执行finalize方法。如果执行过或者没有覆盖finalize方法,则被认为没有必要执行,直接回收。
如果有必要执行finalize方法,这些对象会放置在一个F-Queue的队列中,并在稍后由一个JVM建立的低优先级的finalizer线程去执行。执行时JVM不会等待方法执行结束,因为会出现有的对象finalize方法执行慢或者出现死循环,导致队列中其他对象永久处于等待状态的情况。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
11.java 中都有哪些引用类型?
- 强引用
指在程序代码之中普遍存在的,类似“Object obj=new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。 - 软引用
有用但不是必须的对象,在发生内存溢出之前会被回收。 - 弱引用
有用但不是必须的对象,在下一次GC的时候会被回收。 - 虚引用
无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用的用途是在GC时返回一个通知。
12.说一下 jvm 有哪些垃圾回收算法?
- 标记清除算法
从根集合(GC Roots)开始扫描,对存活对象进行标记。标记完成后再扫描整个空间中未被标记的对象,进行回收。
不需要进行对象的移动,只对不存活的对象进行处理。在存活对象较多的情况下极为高效,但是会产生内存碎片。 - 复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片问题。把堆分成一个对象面和多个空闲面。程序先从对象面为对象分配内存空间,当对象满了,就从GC Roots中扫描活动对象,并把活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。 - 标记整理算法
采用与标记清除算法一样的方式,但是在回收不存活的对象之后,会将所有存活对象往左端的空闲空间移动,并更新对应的指针。成本更高,但是解决了内存碎片问题。 - 分代收集算法
根据对象存活的生命周期,将内存划分为若干个不同的区域。一般情况下划分为老年代和新生代。老年代的特点是每次垃圾回收时都只有少量对象被回收,而新生代的特点是每次垃圾回收时都有大量对象被回收。
新生代回收算法:
(1)新生成的对象首先放在新生代,新生代的目标就是尽可能快速的收集那些生命周期短的对象。
(2)新生代内存按照8:1:1的比例,分为一个Eden区和两个survivor区(survivor0和survivor1)。大部分对象在Eden区中生成。回收时先将Eden区中的存活对象复制到survivor0中,然后清空Eden区。如果survivor0内存满了,就将survivor0和Eden中的存活对象复制到survivor1中,再清空Eden和survivor0。此时survivor0是空的,将survivor0和survivor1互换,保证survivor1是空的。
(3)当survivor1区不足以存放Eden和survivor0中的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就触发一次Full GC,也就是新生代、老年代都进行回收。
(4)新生代的GC称为Minor GC,触发频率较高,不一定等Eden区满了再触发。
老年代回收算法:
(1) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
(2)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
13.说一下 jvm 有哪些垃圾回收器?
- Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。 - Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。 - ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。 - Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。 - Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。 - CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
14.GC是什么时候触发的?
- Scavenge GC
当新的对象产生,在Eden区申请空间失败是,会触发Scavenge GC。清除非存活的对象,并把存活对象移动到survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。 - Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。
有如下原因可能导致Full GC:
(1)年老代被写满。
(2)持久代被写满。
(3)System.GC被调用。
(4)上次GC后堆的各域分配策略动态变化。
15.详细介绍一下 CMS 垃圾回收器?
因为在Hotspot JVM的世代回收过程中,新生代的空间会比较小,而老生代的空间会比较大。基于老生代空间大,变更小的特点,为了尽量减少GC引起的停顿时间,采用了停顿时间最短的CMS收集器。在CMS的并发标记的过程中,它会将整个老生代的空间切割为一个个block,每个block对应一个card。并且对整个老生代加上write barrier。从而在并发的标记过程中,用card来记录堆内发生写操作的区域。
CMS垃圾回收的特点
1.cms只会回收老年代和永久带(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻带;
2.cms是一种预处理垃圾回收器,它不能等到old内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以cms垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%;
CMS垃圾回收步骤
- 初始标记
- 标记老年代中所有的GC Roots对象。
- 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)
- 并发标记
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理; - 预清理阶段
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card - 可终止的预处理
这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻带的引用,是的下个阶段的重新标记阶段,扫描年轻带指向老年代的引用的时间减少; - 重新标记
这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻带的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。 - 并发清理
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。 - 并发重置
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
CMS需要注意的问题
- CMS不是full GC
有一点需要注意的是:CMS并发GC不是“full GC”。HotSpot VM里对concurrent collection和full collection有明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关的,cms收集算法只是清理老年代。 - 减少remark阶段停顿
一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:
-XX:+CMSScavengeBeforeRemark
在执行remark操作之前先做一次ygc,目的在于减少ygen对oldgen的无效引用,降低remark时的开销,如果添加该参数后 ”ygc停顿时间+remark时间<添加该参数之前的remark时间“,说明该参数是有效的; - 内存碎片
CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片,这时候我们需要用到这个参数:
-XX:CMSFullGCsBeforeCompaction=n
CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中,
- UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
- 用户调用了System.gc(),而且DisableExplicitGC没有开启。
- young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。
CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些;这是个取舍。
- concurrent mode failure
这个异常发生在cms正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。
设置cms触发时机有两个参数:
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC。
-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用设定的回收阈值CMSInitiatingOccupancyFraction,则JVM仅在第一次使用设定值,后续则自动调整会导致上面的那个参数不起作用。
总结
优点:
1).将stop-the-world的时间降到最低,能给电商网站用户带来最好的体验。
2).尽管CMS的GC线程对CPU的占用率会比较高,但在多核的服务器上还是展现了优越的特性,目前也被部署在国内的各大电商网站上。
缺点:
1).对CMS在单核和多核机器上做测试。发现CMS在收集过程中会大量占用CPU的时间。所以在第二个阶段会比较漫长,所以一般将其设置在多核机器上。并且对于CMS在单核机器上的表现设计了一套启发式控制。这种控制将收集器看作一个掠夺者,而收集器会尽量赶在用户线程分配新的对象之前完成收集的工作。同样也有可能会出现用户线程希望分配对象,但目前空间不够,则需要停下收集器,这样会让整个收集时间大大加长。所以这时候一搬会选择扩张堆的大小。
2).Mark Sweep算法一直令人诟病的碎片问题,造成了堆空间的浪费以及利用率的下降。
3).需要较大的内存空间去运行,因为在很多并行的阶段,要考虑到用户程序运行时也要分配空间。所以一般选择在堆利用率达到一个常数的时候就开启CMS的收集。可以在VM argument里来设置这个阀值。(–XX:CMSInitiatingOccupancyFraction =n,n=0~100)
4).会产生浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,伴随程序自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理。
16.常用的 jvm 调优的参数都有哪些?
jvm配置
XX比X的稳定性更差,并且版本更新不会进行通知和说明。
1、-Xms
s为strating,表示堆内存起始大小
2、-Xmx
x为max,表示最大的堆内存
(一般来说-Xms和-Xmx的设置为相同大小,因为当heap自动扩容时,会发生内存抖动,影响程序的稳定性)
3、-Xmn
n为new,表示新生代大小
(-Xss:规定了每个线程虚拟机栈(堆栈)的大小)
4、-XX:SurvivorRator=8
表示堆内存中新生代、老年代和永久代的比为8:1:1
5、-XX:PretenureSizeThreshold=3145728
表示当创建(new)的对象大于3M的时候直接进入老年代
6、-XX:MaxTenuringThreshold=15
表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代
7、-XX:-DisableExplicirGC
表示是否(+表示是,-表示否)打开GC日志