JVM+GC解析

目录

一、GVM内存结构

1、JVM体系概览

2、Java内存结构

3、java8以后的jvm

二、常见的垃圾回收算法

1、引用计数算法

2、标记清除算法(追踪回收算法)

3、复制回收算法——针对新生代

4、标记整理算法(压缩回收算法)——针对老年代

5、分代回收算法

三、JVM垃圾回收的时候如何确定垃圾?什么是GC Roots?

1、什么是垃圾?

2、要进行垃圾回收,如何判断一个对象是否可以被回收?

四、JVM调优和参数配置,如何盘点查看JVM系统默认值?

1、JVM的参数类型

2、查看JVM默认值

五、平时工作用过的JVM常用基本配置参数有哪些?

1、java查看虚拟机内存容量和最大内存量

2、JVM常用基本配置参数

3、实操设置JVM参数及收集详细GC日志收集信息

六、强引用/软引用/弱引用/虚引用分别是什么

1、整体架构

2、强引用(默认支持模式)

3、软引用

4、弱引用

5、虚引用

6、软引用和弱引用的适用场景

7、ReferenceQueue

8、GCRoots和四大引用的小总结

七、谈谈你对OOM的认识

1、java.lang.StackOverflowError——栈溢出

2、java.lang.OutOfMemoryError: Java heap space——堆内存不够用

3、java.lang.OutOfMemoryError: GC overhead limit exceeded——GC回收时间过长,频繁GC又没效果

4、java.lang.OutOfMemoryError: Direct buffer memory——直接内存挂了

5、java.lang.OutOfMemoryError: unable to create new native thread——创建线程数量达到上限,不能再创建更多本地线程了

6、java.lang.OutOfMemoryError:Metaspace——元空间满了

八、七大垃圾收集器及其在生产上的配置

1、Serial/Serial Coping收集器(用于新生代)

2、ParNew收集器(新生代)

3、Parallel / Parallel Scavenge收集器(“吞吐量优先”收集器)(新生代)

4、Serial Old收集器(老年代)

5、Parallel Old收集器(老年代)

6、CMS收集器(老年代)

7、G1收集器(Garbage First)

8、怎么查看默认的垃圾回收器是哪个?

9、小结:如何选择垃圾回收器


一、GVM内存结构

1、JVM体系概览

JVM+GC解析_第1张图片

GC作用域:方法区和堆区

2、Java内存结构

JVM+GC解析_第2张图片

(1)PC寄存器/程序计数器(Program Counter Register)
        严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。

(2)Java栈(Java Stack)
        Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。

 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

(3)本地方法栈(Native Method Stack)
      本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

(4)堆 (Heap)
        堆是JVM所管理的内存中最大的一块,是被所有Java线程锁共享的,不是线程安全的,在JVM启动时创建。堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。

(5)方法区(Method Area)
        方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

(6)常量池(Constant Pool)
      常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

3、java8以后的jvm

JVM+GC解析_第3张图片

二、常见的垃圾回收算法

1、引用计数算法

在堆中对每个对象都有一个引用计数器,当对象被引用时,引用计数器加1当引用被置为空或者离开这个作用域时,引用计数减1,这种做法的缺点是每次对对象赋值均要维护引用计数器,并且无法解决相互引用的问题,因此JVM没有采用这个算法。

2、标记清除算法(追踪回收算法)

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,利用JVM维护的对象引用图,从根节点开始遍历对象的引用图,同时标记遍历到的对象,即为可达对象。未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

它主要由两个缺点:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

JVM+GC解析_第4张图片

3、复制回收算法——针对新生代

  为了解决标记清除算法的效率问题,出现了复制算法,它将可用内存按容量划分为大小相等的两块,每次使用其中的一块当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉

优点是每次都是对其中的一块进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点是将内存缩小为原来的一半,代价太高了一点。

JVM+GC解析_第5张图片

现在的商业虚拟机都采用复制收集算法来回收新生代有研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当然,并不能保证每次回收都只有10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。即如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

4、标记整理算法(压缩回收算法)——针对老年代

复制收集算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制收集算法。

根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是把堆中活动的对象移动到堆中一端,这样就会在堆中另外一端留出很大一片空闲区域,相当于对堆中的碎片进行处理

JVM+GC解析_第6张图片

5、分代回收算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并无新的方法,只是根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收

三、JVM垃圾回收的时候如何确定垃圾?什么是GC Roots?

1、什么是垃圾?

简单说就是内存中已经不再被使用的空间就是垃圾。比如说没有引用指向这个对象了

2、要进行垃圾回收,如何判断一个对象是否可以被回收?

1)引用计数法

Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。可通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1每当有一个引用失效时,计数器值减1。任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象
那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题

(2)枚举根节点做可达性分析(根搜索路径)

JVM+GC解析_第7张图片

Java可以做GCRoots的对象:

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

例如:

JVM+GC解析_第8张图片

四、JVM调优和参数配置,如何盘点查看JVM系统默认值?

1、JVM的参数类型

(1)标配参数

  • java -version
  • java -help

(2)X参数

  • java -version:混合模式
  • java -Xint -version:解释执行
  • java -Xcomp -version:第一次使用就编译成本地代码
  • java -Xmixed -version:混合模式,先编译再执行

(3)XX参数

说明:

jps -l 【查看java进程,定位进程编号】

jinfo -flag 某属性 进程id 【查看jvm参数】

jstack 进程id 【查看进程运行情况,可用于排错】

  • A、Boolean类型:【-XX:+或者-某个属性值】  +表示开启,-表示关闭

举例:

是否打印GC收集细节:-XX:+PrintGCDetails  或  -XX:-PrintGCDetails

JVM+GC解析_第9张图片

是否使用串行垃圾收集器:-XX:+UseSerialGC或  -XX:-UseSerialGC

  • B、KV设值类型:【-XX:属性key=属性值value】  

设置Metaspace大小:-XX:MetaspaceSize = 128m

年龄阈值,默认15(对象被复制的次数):-XX:MaxTenuringThreshold = 15

  • C、jinfo举例,查看当前运行程序配置:【jinfo -flag 配置项 进程编号】

举例:jps -l 【获得进程编号】

I、jinfo -flag InitialHeapSize 进程编号  【初始堆内存】

II、jinfo -flags 进程编号

III、jinfo -flag MaxHeapSize 进程编号 【最大堆内存】

  • D、-Xms 和 -Xmx

-Xms等价于-XX:InitialHeapSize 

-Xmx等价于-XX:MaxHeapSize

2、查看JVM默认值

(1)-XX:+PrintFlagsInitial  :查看默认值

java -XX:+PrintFlagsInitial

java -XX:+PrintFlagsInitial -version

(2)-XX:+PrintFlagsFinal :主要查看修改更新

java -XX:+PrintFlagsFinal

java -XX:+PrintFlagsFinal  -version

JVM+GC解析_第10张图片

说明:【“=”是默认值,“:=是修改过后的值”】

(3)PrintFlagsFinal 举例,运行java命令的同时打印出参数

(4)-XX:+PrintCommandLineFlags  -version

JVM+GC解析_第11张图片

五、平时工作用过的JVM常用基本配置参数有哪些?

1、java查看虚拟机内存容量和最大内存量

JVM+GC解析_第12张图片

2、JVM常用基本配置参数

(1)-Xms:初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize 

(2)-Xmx:最大分配内存,默认为物理内存1/4,等价于-XX:MaxHeapSize

(3)-Xss:设置单个线程栈的大小,一般默认为512K~1024K,等价于-XX:ThreadStackSize

(4)-Xmn:设置年轻代大小

(5)-XX:MetaspaceSize:设置元空间大小

典型设置案例:

(6)-XX:+PrintGCDetails:收集详细GC日志收集信息

(7)-XX:SurvivoRatio:设置新生代中eden和s0/s1空间的比例

默认-XX:SurvivoRatio=8,Eden:s0:s1=8:1:1,SurvivoRatio的值就是这只eden区的比例,s0/s1相同

(8)-XX:NewRatio:配置新生代和老年代在堆结构的占比

默认-XX:NewRatio=2,新生代占1,老年代占2,新生代占整个堆的1/3,NewRatio值就是这事老年代的占比

(9)-XX:MaxTenuringThreshold :设置垃圾最大年龄,就是从young到old要经过多少次的垃圾回收

JVM+GC解析_第13张图片

3、实操设置JVM参数及收集详细GC日志收集信息

(1)-Xss查看和修改单个线程栈的大小

奇怪:这里ThreadStackSize为0

JVM+GC解析_第14张图片

设置JVM参数

再次查看

如果是0表示用的是系统出厂默认值,其他值则表示是修改过的

JVM+GC解析_第15张图片

(2)收集详细GC日志收集信息

假设我们现在把初始大小内存和最大内存容量设置为10M,并打印GC日志信息

配置jvm参数:

正常情况下:

JVM+GC解析_第16张图片

现在定义了一个超过10M的数据,会进行GC和Full GC,出现OOM异常

JVM+GC解析_第17张图片

(2)GC和Full GC垃圾回收参数解读

  • GC

JVM+GC解析_第18张图片

  • Full GC

JVM+GC解析_第19张图片

六、强引用/软引用/弱引用/虚引用分别是什么

引用类型 说明 适用场景
强引用(默认支持模式) 对于强引用的对象,就算是出现了OOM也不会对该对象进行回收死都不收  
软引用 当系统内存充足时不会被回收,当系统内存不足时会被回收 高速缓存
弱引用 只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存  
虚引用 在任何时候都可能被垃圾回收器回收 当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM运行我们在对象被销毁后,做一些我们自己想做的事情

1、整体架构

JVM+GC解析_第20张图片

2、强引用(默认支持模式)

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收死都不收

强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)

举例:只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。

现象:obj1你回收你的,obj2是强引用,不会被回收

JVM+GC解析_第21张图片

3、软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,当系统内存充足时不会被回收,当系统内存不足时会被回收

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

举例:内存足够的情况下软引用不会被回收

现象:由于内存足够,此时垃圾回收器不回收软引用

JVM+GC解析_第22张图片

举例:内存不够用的情况下软引用会被回收

配置参数:-Xms10m -Xmx10m。JVM配置,故意产生大对象并配置大内存,让它内存不够了就导致OOM,看软引用的回收情况

现象:由于内存不够了,此时垃圾回收器回收软引用

JVM+GC解析_第23张图片

4、弱引用

弱引用需要用java.lang.WeakReference类来实现,它比软引用的生命期更短。对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存

举例:只要有gc弱引用就会被回收

JVM+GC解析_第24张图片

EG:WeakHashMap

5、虚引用

虚引用需要java.lang.ref.PhantomReference类来实现。如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。java技术允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

举例:虚引用的get方总是返回null;虚引用被回收前需要被引用队列ReferenceQueue保存下

JVM+GC解析_第25张图片

6、软引用和弱引用的适用场景

JVM+GC解析_第26张图片

7、ReferenceQueue

java提供了4中引用类型,在垃圾回收时,都有各自的特点。

ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。

创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列当中,那么就可以在所引用的对象被内存回首之前采取必要的行动,这相当于是一种通知机制

当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM运行我们在对象被销毁后,做一些我们自己想做的事情

8、GCRoots和四大引用的小总结

JVM+GC解析_第27张图片

七、谈谈你对OOM的认识

JVM+GC解析_第28张图片

OOM类型 是什么 产生原因
java.lang.StackOverflowError 栈溢出 如不合理的递归调用
java.lang.OutOfMemoryError: Java heap space 堆内存不够用 如死循环、创建大的对象但是堆内存根本就不够分配
java.lang.OutOfMemoryError: GC overhead limit exceeded GC回收时间过长,频繁GC又没效果 当GC为释放很小空间占用大量时间时抛出。一般是因为堆太小,没有足够的内存。
java.lang.OutOfMemoryError: Direct buffer memory 直接内存挂了 元空间并不在虚拟机中,而是使用本地内存。不归GC管,所以GC不会回收,如果本地内存用完了就会抛出此异常。
java.lang.OutOfMemoryError: unable to create new native thread 创建线程数量达到上限,不能再创建更多本地线程了 应用创建太多线程,超过系统承载能力
java.lang.OutOfMemoryError:Metaspace 元空间满了

虚拟机加载的类信息、常量池、静态变量、即时编译后的代码不断往元数据空间灌,占据的总空间超过了Metaspace指定的元空间大小

1、java.lang.StackOverflowError——栈溢出

产生的原因:不合理的递归调用【过深的递归调用】

JVM+GC解析_第29张图片

2、java.lang.OutOfMemoryError: Java heap space——堆内存不够用

产生的原因:死循环

JVM+GC解析_第30张图片

产生原因:超过堆内存大小限制

配置参数:-Xms10m -Xmx10m

JVM+GC解析_第31张图片

3、java.lang.OutOfMemoryError: GC overhead limit exceeded——GC回收时间过长,频繁GC又没效果

程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高

GC回收时间过长时会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。连续多次GC都只回收了不到2%的极端情况下才会抛出。

假如不抛出 GC overhead limit 错误会发生什么情况:那就是GC清理的这么点内存很快就会再次填满,迫使GC再次执行。这样就造成恶性循环,CPU使用率一直是100%,而GC却没有任何成果。

JVM+GC解析_第32张图片

产生的原因:当GC为释放很小空间占用大量时间时抛出。一般是因为堆太小,没有足够的内存。虽然在做GC,但基本上没回收

配置参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m 

JVM+GC解析_第33张图片

解决方法:

(1)首先检查程序有没有死循环或者其他一些导致内存被大量占用的程序,如果确定程序没有问题,只是程序本身需要大内存时,通过设置增加内存。

(2)添加jvm启动参数限制使用内存:-XX:UseGCOverheadLimit

4、java.lang.OutOfMemoryError: Direct buffer memory——直接内存挂了

产生的原因:元空间并不在虚拟机中,而是使用本地内存。不归GC管,所以GC不会回收,如果本地内存用完了就会抛出此异常。【对象没有new在堆内存,而是在本地内存】

JVM+GC解析_第34张图片

配置参数:-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m 

JVM+GC解析_第35张图片

5、java.lang.OutOfMemoryError: unable to create new native thread——创建线程数量达到上限,不能再创建更多本地线程了

高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError: unable to create new native thread,准确地讲该native thread异常与应用的平台有关

JVM+GC解析_第36张图片

运行结果:

非ROOT用户登录Linux系统测试->服务器级别参数调优:

JVM+GC解析_第37张图片

JVM+GC解析_第38张图片

6、java.lang.OutOfMemoryError:Metaspace——元空间满了

java8之后使用Metaspace来代替永久代。Metaspace是在方法区HotSpot中实现的,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存中它用来存放虚拟机加载的类信息、常量池、静态变量、即时编译后的代码MaxMetaspaceSize初始化大小是20M左右。

产生原因:不断生成类往元数据空间灌,类占据的总空间超过了Metaspace指定的元空间大小

配置参数:-XX:MetaspaceSize=10m  -XX:MaxMetaspaceSize=10m

JVM+GC解析_第39张图片

八、七大垃圾收集器及其在生产上的配置

GC算法(引用计数/复制/标记/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。

因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最适合的收集器,进行分代收集

七种主要的垃圾收集器:

JVM+GC解析_第40张图片    

垃圾收集器类型 串行/并行/并发 新生代/老年代 详细说明 算法 目标 适用场景
串行垃圾回收器(Serial) 串行 新生代 为单线程环境设计并且只适用一个线程进行垃圾回收,会暂停所有的用户线程 复制算法 响应速度优先 适用于单CPU环境,不适合服务器环境
Serial Old 串行 老年代 现在已经没有了 标记-整理算法 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
并行垃圾回收器(Parallel) 并行 新生代 多个垃圾回收器并行工作,此时用户线程是暂停的 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 同上 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务,如科学计算/大数据处理
Parallel Old 并行 老年代 同上 标记-整理算法 吞吐量优先 在后台运算而不需要太多交互的任务,如科学计算/大数据处理
并发垃圾回收器(CMS) 并发 老年代 用户线程和垃圾收集器同时执行(不一定是并行,可能交替执行),不需要停顿用户线程 标记-清除算法 响应速度优先 适用于对响应时间有要求的场景,集中在互联网站或B/S系统服务端上的Java应用
G1垃圾回收器 并发 both 将堆内存分割成不同的区域然后并发地对其进行垃圾回收 标记-整理+复制算法 响应速度优先 面向服务端应用,将来替换CMS

1、Serial/Serial Coping收集器(用于新生代)

单线程,在进行垃圾收集时必须暂停其他所有的工作线程,使用复制算法。虚拟机运行在Client模式下的默认新生代收集器。简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,由于没有线程交互的开销可以获得最高的单线程垃圾收集效率因此Serial垃圾收集器依然是java虚拟机在client模式下默认的新生代垃圾收集器。

对应的JVM参数是:-XX:+UseSerialGC

开启后会使用:Serial(Young区用)+Seiral Old(Old区用)的收集器组合

表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法

JVM参数配置:

-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

2、ParNew收集器(新生代)

ParNew收集器其实是Serial收集器的多线程版本,采用复制算法,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作

对应的JVM参数是:-XX:+UseParNewGC

启用ParNew收集器,只影响新生代的收集,不影响老年代,开启上述参数后,会使用:ParNew(Young区用)+Seiral Old(Old区用)的收集器组合,新生代使用复制算法,老年代采用标记-整理算法。但是这样的组合java8已不再推荐使用

JVM参数配置:

-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseParNewGC

3、Parallel / Parallel Scavenge收集器(“吞吐量优先”收集器)(新生代)

使用复制算法,并行多线程,这些特点与ParNew一样

  • Parallel Scavenge收集器的目的是达到一个可控制的吞吐量(Throughput),即CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,吞吐量就是99%。
  • 自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量

停顿时间越短对于需要与用户交互的程序来说越好,良好的响应速度能提升用户的体验;高吞吐量可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不太需要太多交互的任务

对应的JVM参数是-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活)

启用Parallel Scavenge收集器,开启上述参数后,会使用:Parallel Scavenge(Young区用)+ Parallel Old(Old区用)的收集器组合,新生代使用复制算法,老年代采用标记-整理算法。

JVM参数配置:

-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseParallelGC 
-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseParallelOldGC 

其他参数设置:

-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间。(大于0的毫秒数)停顿时间缩短是以牺牲吞吐量和新生代空间换取的。(新生代调的小,吞吐量跟着小,垃圾收集时间就短,停顿就小)。

-XX:GCTimeRatio 直接设置吞吐量大小,0

-XX:+UseAdaptiveSizePolicy  一个开关参数,开启GC自适应调节策略(GC Ergonomics),将内存管理的调优任务(新生代大小-Xmn、Eden与Survivor区的比例-XX:SurvivorRatio、晋升老年代对象年龄-XX: PretenureSizeThreshold 、等细节参数)交给虚拟机完成。这是Parallel Scavenge收集器与ParNew收集器的一个重要区别,另一个是吞吐量。

4、Serial Old收集器(老年代)

它是Serial收集器的老年代版本单线程,使用“标记-整理”算法。主要意义是被Client模式下的虚拟机使用。

如果在Server模式下,它还有两大用途:

  • 在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;
  • 作为CMS 收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。运行过程同Serial收集器。

5、Parallel Old收集器(老年代)

它是Parallel Scavenge收集器的老年代版本多线程,使用“标记-整理”算法在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器

6、CMS收集器(老年代)

它是一种以获取最短回收停顿时间为目标的收集器,尽可能缩短垃圾收集时用户线程的停顿时间。优点:并发收集,低停顿。基于“标记-清除”算法

目前很大一部分Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常适用于堆内存大、CPU核数多的服务端应用。运作过程分为4个步骤:

  • 初始标记(CMS initial mark):需要“Stop The World”,标记GC Roots能直接关联到的对象,速度快。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing 过程,和用户线程一起工作。主要标记过程,标记全部对象
  • 重新标记(CMS remark):需要“Stop The World”,修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。停顿时间:初始标记<重新标记<<并发标记
  • 并发清除(CMS concurrent sweep):清除GC Roots不可达对象,和用户线程一起工作。对于标记结果,直接清理对象,时间较长。

对应的JVM参数是-XX:+UseConcMarkSweeoGC 

启用UseConcMarkSweeoGC 收集器,开启上述参数后,会自动将-XX:+UseParNewGC打开,使用:ParNew(Young区用)+ CMS(Old区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器,新生代使用复制算法,老年代采用标记-清除算法。

JVM参数配置:

-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseConcMarkSweeoGC 

优点:并发收集低停顿

缺点:

  • 并发执行,对CPU资源压力大:由于并发进行,CMS在收集与应用程序会同时增加对堆内存的占用,也就是说,CMS必须在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败,将触发担保机制,串行老年代收集器将会以SWT的方式进行一次GC,从而才造成较大停顿时间。CMS默认的回收线程数: (CPU数量+3)/4
  • 无法处理浮动垃圾:可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。并发清理阶段用户程序运行产生的垃圾过了标记阶段所以无法在本次收集中清理掉,称为浮动垃圾。CMS收集器默认在老年代使用了68%的空间后被激活。若老年代增长的不是很快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction  提高触发百分比,但调得太高会容易导致“Concurrent Mode Failure”失败。
  • 基于“标记-清除”算法会产生大量空间碎片:老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。提供开关参数-XX:+UseCMSCompactAtFullCollection 用于在“ 享受”完Full GC服务之后进行碎片整理过程,内存整理的过程是无法并发的,停顿时间会变长。-XX:CMSFullGCsBeforeCompation 设置在执行多少次不压缩的Full GC后,进行一次带压缩的。

7、G1收集器(Garbage First)

(1)以前收集器特点

  • 年轻代和老年代都是各自独立且连续的内存块
  • 年轻代收集使用单eden+S0+S进行复制算法
  • 老年代收集必须扫描整个老年代区域
  • 都是以尽可能少而快地执行GC为设计原则

(2)G1是什么及其特点

G1可解决内存碎片问题。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大减少垃圾收集的停顿时间。主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成一个个大小一样的region,每个region从1M到32M不等。一个region可能属于Eden,Survivor或Tenured的内存区域

特点:

JVM+GC解析_第41张图片

(3)底层原理

G1区域化垃圾回收器:

JVM+GC解析_第42张图片

JVM+GC解析_第43张图片

JVM+GC解析_第44张图片

JVM+GC解析_第45张图片

做大的好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可

回收步骤:

G1收集器下的Young GC

JVM+GC解析_第46张图片

JVM+GC解析_第47张图片

4步过程:

JVM+GC解析_第48张图片

常用参数配置:

JVM+GC解析_第49张图片

-Xms10m -Xmx10m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseG1GC 
 

与CMS相比有两个显著改进:

  • 消除内存碎片
  • 可以精确地控制停顿

8、怎么查看默认的垃圾回收器是哪个?

JVM参数:java -XX:+PrintCommandLineFlags -version

JVM+GC解析_第50张图片JVM+GC解析_第51张图片

9、小结:如何选择垃圾回收器

JVM+GC解析_第52张图片JVM+GC解析_第53张图片

JVM+GC解析_第54张图片JVM+GC解析_第55张图片

你可能感兴趣的:(java面试题,内存结构,OOM,JVM参数,垃圾回收,JVM调优)