JVM内存结构和垃圾回收简介
JVM相关知识是性能测试必须要了解的,同时也是面试中经常遇到的问题。
JVM内存管理机制
JVM简介
- Java采用了自动管理内存的方式
- Java程序是运行在JVM之中的
- Java的跨平台是基于JVM的跨平台特性
- 内存的分配和对象的创建是在JVM中
- 用户可以通过一系列参数来配置JVM
JVM运行时区域
JVM内存结构
一、栈内存
- 线程私有
- 生命周期和线程相同
- 主要存放内容:
- 基本数据类型(int,char,float,double...)
- 对象的引用,指向了对象在堆内存中起始地址
- 通过-Xss参数 配置,设置每个线程的堆栈大小
二、堆内存
- 堆内存是JVM中空间最大的区域
- 所有线程共享堆
- 所有的数组以及内存对象的实例都在此区域分配
- 堆内存大小通过参数进行配置:
- -Xmx:最大堆内存
- -Xms:最小堆内存,此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
- -Xmn:设置年轻代大小
- 堆内存的构成,如上图所示:
- 新生代:包括三块区域,eden、from survivor(s0)、to survivor(s1)
- 老年代:old gen
- 堆内存数据变化:
1、初始化对象首先放在新生代eden区
2、eden区满了之后,对eden区进行扫描,将仍然存活的对象移到s0区,同时对eden区和是s1区进行清扫工作
3、新创建的对象继续存在eden区,eden满了之后,再次扫描eden和s0区,将存活的对象放在s1区,将eden和s0清空
4、新创建的对象继续存在eden区,eden满了之后,再次扫描eden和s1区,将存活的对象放在s0区,将eden和s1清空,如此循环往复。(如果对象在新生代中存活15次,存入老年代)
5、如果从eden区和s0区存入s1时,s1放不下,则将对象放在老年代中,将整个新生代全部清空。
(备注:线程结束,对象的实例失去引用,在栈内存中没有地址,则认为对象无用)
- 栈内存和堆内存举例:
Object o = new Object()
其中,o存放在栈内存中,new Object()存在在堆内存中,变量o是Object对象的引用,o上存放了Object对象占用内存的起始地址
三、永久代
- 永久代也叫(Method Area)
- 各线程共享,主方法区要存放类信息、常量、静态变量,如public static int a = 2
- 垃圾回收行为比较少见
四、JVM结构总结
Survivior = s0+s1
年轻代 = Eden + Survivor
年轻代 = Eden + s0 + s1
堆内存 = 年轻代 + 老年代
堆内存 = Eden + s0 + s1 + 老年代
五、Java8新变化
Java8从JVM中移除了PermGen,使用Metaspace(元空间)来代替永久代
- Metaspace不存在JVM中,而是存在本地内存中
- 配置元空间初始值和最大值参数:
- -XX:MetaspaceSize=64m
- -XX: MaxMetaspaceSize=64m
JVM垃圾回收
一、YoungCG和FullGC
新生代引发的GC叫YoungCG,上面内存结构的时候我们提到eden区向s0区存放对象后,清空eden区和s1区就是YoungGC
老年代引发的GC叫FullGC。FullGC会引起整个JVM的用户线程暂停,待垃圾回收完毕后,才继续运行。老年代引发的GC会引起整个堆内存的全局GC。GC的时间长短跟堆内存空间大小有关系。
二、永久代(元空间)的垃圾回收
主要回收:
- 废弃的常量
- 无用的类:
- 类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类的Class对象没有在任何地方被引用
三、堆垃圾回收算法
1、标记-清除算法
(1)分为标记和清除两个阶段,标记完成后,统一回收
(2)标记和清除过程的效率都不高,而且标记清除后会产生大量不连续的内存碎片,如下图
2、复制算法
(1)内存分为相等的两块,当一块的内存用完,将存活对象复制到另外一块,将原内存一次性全部清理掉。复制的时候按顺序分配内存,无内存碎片问题,如下图。
(2)将内存分为两半,空间永远只能使用1/2,利用率低
(3)新生代垃圾回收使用此算法
3、标记-压缩算法
(1)先对存活对象进行标记,让存活对象向一边移动,然后清理掉存活对象边界外的所有内存
(2)老年代垃圾回收使用的就是此算法
4、分代收集算法
新生代YoungGC采用复制算法
老年代FullGC采用标记-压缩算法
四、垃圾收集器
垃圾收集器是内存回收算法的具体实现,JVM不同的区域可以采用不同的垃圾收集器组合,主要有以下几种:
1、Serial收集器(串行)
(1)单线程收集器,串行
(2)JVM中用户线程全部停止
(3)Client模式下,新生代默认使用此收集器(机器配置较低情况下)
(4)简单、高效(机器配置比较低的情况下,单线程可能比多线程效率高)
2、ParNew收集器(并行)
(1)并行收集器,Serial收集器的多线程版本
(2)Server模式下JVM默认的新生代收集器
(3)默认开启的垃圾回收线程与CPU核数一致
3、CMS收集器(ConcurrentMarkSweep)(并发)
(1)采用了标记-清除、标记-压缩算法
(2)并发收集、低停顿
(3)会消耗cpu、会产生内存碎片、会有浮动垃圾(Concurrent Mode Failure)
(浮动垃圾:并发清理的时候用户线程还在运行,清理的同时还在产生新垃圾,必须在空间占用70%-90%的时候就进行垃圾回收,防止浮动垃圾产生的速度大于并发清理的速度)
(4)收集器不重:
<1>初始标记:只会标记根对象,这个过程很快,此时用户线程会停掉;
<2>并发标记:通过根对象搜索,跟用户线程一起操作;
<3>重新标记:用户线程也会停掉,标记第<2>步新产生的垃圾,速度很快
五、内存溢出
1、内存溢出
堆内存溢出(OutOfMemory:Java heap space):堆内存中存在大量对象,这些对象都有被引用。当所有对象占用空间达到堆内存的最大值,就会出现内存溢出。
永久代溢出(OutOfMemoryError:PermGen space):类的一些信息,例如类名、访问修饰符、字段描述、方法描述等,所占空间大于永久代最大值,就会出现溢出。如果存在问题,程序启动阶段就会报错。
2、内存溢出检测方法
(1)图形界面:Jconsole、Jvisualvm
(2)命令行工具:
Jstat –gcutil pid 1000 100
Jmap –histo pid | head -20
Jmap –heap pid
六、JVM常见参数
1、-Xms2048m,初始堆大小,建议<物理内存的1/4,默认值为物理内存的1/64
2、-Xmx2048m,最大堆大小,建议与-Xms保持一致,默认值为物理内存的1/4
3、-Xmn512m,新生代大小,建议不超过堆内存的1/2
4、-Xss256k,线程堆栈大小,建议256k
5、-XX:PermSize=256m,永久代初始值,默认值为物理内存的1/64
6、-XX:MaxPermSize=256m,永久代最大值,默认值为物理内存的1/4
7、-XX:SurvivorRatio=8,新生代中eden区和Survivor区的比例,默认为8:1,即Eden(8),From Space(1),To Space(1)
8、-XX:UseConcMarkSweepGC,开启CMS垃圾回收器
CMS相关参数
-XX:+UseConcMarkSweepGC:默认关闭,ParNew+CMS+Serial Old,当CMS收集器出现 ConcurrentModeFailure错误(Jvm预留空间不足以容纳程序使用),采用后备收集器Serial Old -XX:CMSInitiatingOccupancyFraction=80:CMS收集器在老年代空间被使用多少时触发FullGC,默认为92
-XX:+UseCMSCompactAtFullCollection:CMS收集器在FullGC时开启内存碎片的压缩,默认关闭
-XX:CMSFullGCsBeforeCompaction=8:执行多少次不压缩FullGC后,进行一次压缩,默认是0(代表每次 FullGC都进行压缩)
-XX:+UseCMSInitiatingOccupancyOnly:使用手动定义初始化定义开始,禁止hostspot自行触发CMS GC
-XX:ParallelGCThreads=8:并行收集器的线程数,此值最好配置与处理器数目相等 同样适用于CMS
日志参数:
-XX:+HeapDumpOnOutOfMemoryError:当发生内存溢出时,进行堆内存dump-XX:+PrintGCDetails:打印 GC的详细信息码同学 码同