JVM内存模型与GC
一.脑图
二..JVM内存模型
JAVA虚拟机在运行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途以及创建和销毁的时间,有的区域会随着虚拟机进程的启动而存在,有的则依赖用户线程的启动和结束而随着建立和销毁 . 总结下来 : JVM虚拟机所管理的内存主要包括以下几个运行时数据区域
![](https://img2022.cnblogs.com/blog/1162739/202203/1162739-20220324163326331-344013503.png)
1. 程序计数器(Program Counter Register)
一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器. 字节码在工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成 .
java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的 . 在任何一个时刻,一个处理器都会执行一条线程中的指令 . 因此 , 为了线程切换后能恢复到正确的执行位置 , 每条线程都需要有一个独立的程序计数器.且各线程的程序计数器独立存储,互不影响 . 这块每个线程独有的内存我们成为线程私有内存 .
若当前在执行java方法, 则当前程序计数器记录的是正在执行的字节码指令的地址 , 如果执行的是native方法 , 计数器中为空(undefined) .
注 : 程序计数器是在虚拟机规范中没有规定任何OOM的区域
2.虚拟机栈(VM Stack)
与程序计数器类似 , 虚拟机栈也是线程私有的 , 且它的生命周期与线程相同.
虚拟机栈描述的是java方法执行的内存模型 , 即: 每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量、操作数栈、动态链接、方法出口等信息 . 每一个方法的开始执行到结束,都对应一个栈帧在虚拟机中入栈到出栈的过程 .
2.1局部变量表
此处存储:
- 编译期可知的各种基本数据类型
- 对象引用(此处存储的不是对象本身,可能指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他于此对象相关的位置)
- returnAddress类型(指向一条字节码指令的地址)
其中 , long和double类型的数据会占用两个局部变量空间(2 Slot),其余数据类型只占用一个(1 Slot),当进入一个方法时 , 这个方法所需要在帧中分配多大的局部变量空间是完全确定的 , 且在方法运行期间不再改变 .
2.2 定义的异常类
- StatckOverFlowError : 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出该异常 .
- OOM(Out Of Memory Error) : 当前大部分虚拟机栈都允许动态扩展(也允许固定大长度的虚拟机栈) , 若扩展时无法申请到足够的内存,就会抛出OOM异常
3. 本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈的使用方式大致相同 , 只不过虚拟机栈为虚拟机执行java方法(字节码)服务 . 而本地方法栈则为虚拟机使用到的native方法服务,且java规范并未对native方法使用的语言、使用方式与数据结构强制规定 .
其异常内容同虚拟机栈一样 .
4. 堆(Heap)
堆内存是虚拟机所管理的内存中最大的一块 .java堆是被所有线程共享的一块内存区域 , 在虚拟机启动时创建(项目部署时可定义其最小最大值) , 堆内存唯一的目的就是存放对象实例 . 几乎所有的对象实例都在堆(Heap)中分配内存 (随着JIT的发展,栈上分配、标量替换优化技术使得这一点也不再绝对 . )
java堆内存(Heap)是垃圾回收器的主要管理区域 . 因此也称之为GC堆 .
4.1新生代和老年代
从内存回收的角度看 , 由于垃圾回收基本都在采用分代收集算法 . 所以堆内存还可细分为 : 新生代和老年代
再细致则 : Eden空间、From Surviver空间、ToSurviver空间
默认情况下堆内存的空间分配
- 老年代: 存储被引用较多即活跃度叫高的对象.
- 三分之二的堆空间
- 新生代:
- 三分之一的堆空间
- Eden区 : 8/10的年轻代堆空间
- survivor0 : 1/10的新生代内存空间
- survivor1 : 1/10的新生代内存空间[可理解为进行复制算法清除时的空间]
对象刚初始化时都是在新生代的Eden区里面的,随着GC的时候,若该对象被频繁引用[即活跃度较高],将会迁移到 survivor0里面 , 当GC达到一定次数且该对象依然不被回收 , 则将会归属到老年代中 .
4.2分配缓冲区
线程共享的java堆内存中可能划分出多个线程私有的分配缓冲区 .
5. 方法区(Method Area)
同堆(Heap)一样 , 是各个线程共享的内存区域 , 用于存储已被虚拟机加载的类信息 , 常量 , 静态变量 , 即时编译后的代码等数据 .
5.1 回收方法区
GC的主要操作目标是堆内存 , 而方法区中也有存在垃圾回收的需要 , 只不过其性价比(效率)较低
方法区在HotSpot虚拟机中也称为永久代 , 针对这块内存主要进行回收的目标有 : 废弃常量和无用的类 . 即若有一常量已经进入了常量池 , 但是在系统中没有目标指向该常量即无对象引用该常量 , 则会发生GC .
6. 运行时常量池
是方法区的一部分 ,.Class文件中的除了有类的版本、字段、方法、接口等描述信息外, 还有一项就是常量池 , 用于存放编译器生成的各种字面变量和符号引用 . 这部分在类加载后存入方法区的常量池中
7. 直接内存
并不是虚拟机运行时数据区的一部分 . 也不是java规范中定义的内存 .
在JDK1.4中 , 引入了NIO的概念 , 即一种基于通道与缓冲区的I/O方式 , 它可有使用native函数库直接分配堆外内存 , 然后通过存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作 . 避免在java堆和Native堆中来回复制数据 . 但受限于java堆内存设置大小及机器的内存大小 , 当内存使用总和大于机器本身限制时 , 会抛出OOM异常 .
- 内存相关命令 :
- java -XX:+PrintFlagsFinal -version : 可查看所有的默认的JVM参数
- -XX:InitialSurvivorRatio: 新生代中Eden/Survivor空间的初始比例
- -XX: NewRatio: Old区和Yong区的内存比例
- JDK1.7开始JVM就没有永久代了, 1.7开始JVM把字符串常量池从永久代剥离出来 , 存放在堆空间中
三. 对象
3.1对象在虚拟机中创建的完整过程
- 虚拟机去检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,并且检查该类是否已被加载、解析、初始化.如果没有,先去执行相应的类加载部分 .
- 类加载通过后 , 虚拟机为对象分配内存 . 内存大小在对象加载完成后就会确定,为对象分配内存的过程等同于把一块确定大小的内存从java堆中划分出来 .
- 内存分配完成后 , 虚拟机需要将分配出的内存初始化为0(对象头除外).这一步保证了对象的实例字段在java代码中不赋予初始值的时候就可以使用 . 程序这时能访问到这些字段数据类型的零值 .
- 虚拟机堆对象进行必要设置 , 将以下信息存储在对象的对象头中 . (Object Header)
- 包括该对象是哪个类的实例
- 如何才能找打类的元数据信息
- 对象的hash码
- 对象的GC分代年龄
- 执行init方法 , 把对象按照预期编码的方向进行初始化 .
3.1.1指针碰撞
若当前java内存为规整的 , 那么分配内存就是把对应的分界指针指向空闲区域那边挪动与对象大小相同的距离
3.1.2空闲列表
若java内存不是规整的 , 虚拟机就会维护一个列表 , 记录上可用内存块 , 在分配的时候寻找一个足够大的内存空间划分给对象实例 . 并更新表中的记录 .
注 : java内存是否规整取决于所选择的垃圾回收器是否带有压缩整理功能 .
因此 , 在使用Serial 、 ParNew等带Compact过程的收集器时 , 采用的内存分配方法是指针碰撞 . 使用CMS这种基于Mark-Sweep(标记清除)算法的收集器时,采用的是空闲列表方式 .
为确保在内存分配中并发出现线程不安全的情况 , 可针对每个线程在java堆中预先分配一小块内存,即本地线程分配缓冲(Thread Local Alocation Buffer),当哪个线程需要分配内存时 , 就在哪个TLAB中分配.当TLAB用完时 , 才去进行同步锁定.
虚拟机中是否使用TLAB: 可配置 : -XX:+/-UseTLAB参数来设定 .
3.2对象的内存布局
在HotShot虚拟机中 , 对象在内存中的布局可以分为3块区域 : 对象头(Object Header)、实例数据(Instance Data)和对其填充(Padding)
3.2.1对象头
包括 :
1 .Mark Word: 用于存储对象自身运行时的数据,如哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程、偏向时间戳等 .这部分为非固定的数据结构以便在极小的空间内存储尽量多的信息 .
若为数组 , 对象头中还需有一块记录数组长度的数据 .
2 .类型指针: 对象指向它的类的元数据的指针 . 通过该指针确定对象是哪个类的实例 .
注 : 并不是所有的虚拟机实现都必须在对象数据上保留类型指针 .
四. 垃圾回收
4.1. 判断对象是否可回收?
4.1.1 引用计数器
JVM会给对象添加一个引用计数器,当有一个地方引用时 , 计数器+1;当引用失效时 , 引用值-1;当任何时刻计数器为0时,这个对象则判断为不可能在被使用的或当前没有被使用 . 此时判断为可回收 .
缺点 : 当循环依赖时 ,即便此时无线程在使用该对象 , 但是因为两个对象之间都存在相互引用 , 即此时的计数器都不为0 . 系统认为该对象还处于引用状态 , 不会进行GC回收
注 : 目前虚拟机通常不会以引用计数法进行GC判断是否可回收 .
4.1.2 可达性分析法
基本思想 : 通过一系列的"GC Roots"的对象作为起始点 , 从这些节点开始向下搜索, 搜索的节点路径称之为引用链("Reference Chain"), 当一个对象没有任何引用链可以到达到GC Roots处时, 此时成为对象不可达 , 则证明该对象当前是不可用的 . 这就是可达性分析 .
而对象是否进行GC回收 .要经过两次的判断 .
- 第一次进行分析之后 , 若为不可达 , 则JVM会给该对象打上标记标明其为不可达的 .
- 虚拟机会对不可达的对象进行筛选 , 即筛选该对象是否有必要进行finalize()方法 ,. 当对象没有覆盖该方法或该方法已被虚拟机调用过 . 虚拟机视这两种情况均为没有必要执行GC . 即不进行GC回收 .
而JVM中 , 可作为GC Roots判断的对象包括 :
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(一般指native方法)引用的对象
4.1.3 关于引用
JDK1.2之后 , java对引用的概念进行了扩充 , 将引用分为: 强引用 , 软引用 , 若引用 , 虚引用这4种 . 这四种引用强度依次减弱 .
- 强引用 : 即在程序中普遍存在的 , 例如通过new 关键字声明获取实例的这类引用 , 只要强引用还存在 , 那么垃圾回收器就不会回收 .
- 软引用: 用于描述一些还有用但非必须的对象 . 对于软引用关联的对象 , 在系统即将OOM之前 , 将会把这些对象列入回收范围内进行回收 . 若回收之后还存在内存溢出的情况 , 则会抛出OOM Error异常 .
注 : 软引用可通过SoftReference进行实现 .
- 弱引用 : 也是用来描述非必须对象的 , 但其强度比软引用要弱一些 , 被弱引用关联的对象的生命周期只能到下一次垃圾回收之前 . 当垃圾回收器进行GC时 , 会把当前被弱引用关联的对象进行回收 .
注 : 弱引用 .可通过WeakReference类来实现弱引用
- 虚引用 : 也称为幽灵引用或幻影引用 , 是最弱的一种关系 , 一个对象是否有虚引用的存在 , 完全不影响其生命周期 , 也不会对其生存时间构成影响 , 同样也无法通过虚引用获取到对应对象的实例 . 设置虚引用的最大目的就是在进行GC回收的时候能收到一个系统通知 .
注 : 虚引用可通过PhantomReference进行实现 .
4.2 垃圾回收算法
4.2.1 标记-清除(算法的基础)
分两步 :
1. 标记: 通过可达性分析标记出所有可达的对象 .
2 . 回收 : 循环所有对象 , 将所有不可达的对象[没有标记的对象]进行回收(此时的清除只是把对象的地址维护到空闲列表里,并不是真正的置空该对象,类似于电脑文件的删除,只是把目录清空了)
不足 :
1. 效率较差: 标记阶段需要进行堆空间的全遍历 , 清除阶段需要再进行一次 . 两次全遍历耗时较久
2. 内存空间问题 :
2.1 容易生成不连续的内存碎片 , 内存碎片太多容易造成在后续程序运行过程中需要分配较大对象的内存时 , 无法找到足够的连续内存而提前触发垃圾回收 .
2.2 使用该方式进行清除时需要维护个空闲列表,将没有标记的对象地址维护到空闲列表中.
4.2.2 复制算法
1. 将整个可用内存分为两块 ,每次只使用其中一部分
2. 遍历堆空间对象 , 可达性分析将可达的对象进行打标
3. 将可用对象[可达的对象]复制到另外一块内存中[完整复制,并不只是复制内存地址]
4. 然后再把前一块内存进行统一回收清理 , 这样每次都是对整个半块内存进行回收 , 且可以保证内存的连续性 .
5. 解决了标记-清除中两次全遍历的耗时问题和回收过程中产生的内存碎片的问题.但是内存消耗变大了.
缺点 :
1. 将整体原有的可用内存减小一半 . 其对服务器内存要求较高
2.对于使用G1回收器的来说 , 因为维护了大量的分区,堆空间进行复制后 ,对象地址变化需要同步修改栈中的内存引用地址 , 这个过程需要的内存消耗也比较大 .
4.2.3 标记-整理
步骤 :
- 先进行GC可达性分析, 将可达的对象进行标记.
- 对内存结构进行整理 , 把可达的对象向一端进行移动 , 然后直接清理掉端边界以外的内存 .
- 同比复制算法,整理过程相对要慢,但是相对来说内存需求没那么大
4.2.4 分代收集
根据对象存活周期的不同分为几块 :一般分为新生代和老年代 .
在新生代中 , 每次垃圾回收都发现有大量的对象死去 , 只有少量存活 , 那就使用复制算法 .
老年代中因为对象存活率高,没有额外的空就按对它进行分配和担保 , 就必须使用"标记-整理"或者标记-清理算法进行回收 .
4.2.5 分区算法
区别于分代算法 , 分代算法根据对象的生命周期划分新生代和老年代
分区算法则是根据堆空间的大小划分不同的region,这样每次回收时只需回收堆空间的一部分region即可 . 这样相对减少GC时的停顿时间
4.2.6 算法实现
- 枚举根节点
目前主流的java虚拟机都是使用的准确式GC: 即当系统停顿下来后 , 进行可达性分析时 , 不再去逐个进行分析检查所有执行上下文和全局的引用位置 , 这样会大量的消耗系统性能 , 虚拟机本身会提前知道哪些地方存放着对象引用.
在HotSpotVM中 , 是使用一组称之为OopMap的数据结构来达到这个效果 .
即在类加载完成后 , JVM会把对象内什么偏移量上是什么类型的数据给计算出来 , 在JIT编译过程中 , 也会在特定的位置记录下栈和寄存器中哪些位置是引用 , 这样 , 在进行GC扫描的时候就可以直接得知这些信息了 .
- 安全点
在有了OopMap的支持下 , HotSpot可以快速且准确的完成GC Roots枚举 ,
但是有一个很现实的问题 : 就是在这个过程中很可能会导致引用关系的变化 . 且OopMap内容变化的指令有很多 , 如果为每条指令都生成OopMap , 这将会大量消耗系统内存.增加GC成本 .
为了解决上述问题 , HotSpot引入了安全点的概念 , 上述有说到JVM在进行GC分析的时候 , 是在特定的位置进行分析而不是所有的引用位置进行分析 , 而这些特定的位置即为安全点 . 即程序执行时并非在所有的位置都能停下来开始进行GC , 只有在达到安全点的时候才暂停进行GC .
安全点生成的原则 :
- 即不能太少以至于让GC等待时间太长
- 不能过于频繁以至于GC次数过多增加运行负载 .
- 安全点选定标准 :
- 程序是否具有长时间执行的特征 .
- 因为每条指令执行的时间都非常短暂 , 程序不太可能因为指令流长度太长而过长时间运行.
- 长时间运行的最明显特征就是指令序列复用 . 例如方法调用 , 循环跳转 , 异常跳转等. 而具备这些特征的指令集将被标记为安全点
- 抢先试中断
- 在进行GC时,不需要线程的执行代码主动去配合, 在GC发生时,首先会把线程全部中断 , 如果有的线程不在安全点上 , 则恢复线程直到线程跑到安全点上 . 该方式现在几乎没有JVM采用
- 主动式中断
- 即当GC需要中断线程时 , 不直接对线程进行操作 , 仅仅简单的在线程上设置一个标志 ,线程在执行的过程中会去主动轮询这个标志 , 发现中断标志为真的时候 , 自己主动挂起,以便进行GC回收 .
- 安全区域(Safe Region)
安全区域指 : 在一段代码片段中,引用关系不会发生变化 , 在这个区域的任何地方开始GC都是安全的 . 可以把安全区域视为安全点的扩展 .
- 即针对线程等待或者线程在队列中期间 , JVM可能会误触发主动式中断 , 为避免这一问题 , 当线程执行到安全点的时候 , 会首先标识自己进入了安全区域 , 那样在JVM发起GC时 , 就不需要再考虑线程的安全点问题了 .当线程离开安全区域时 , 会检查安全点是否已经完成了GC回收 , 若已完成 , 则继续执行 . 否则就一直等待直到收到GC完成通知可以离开安全区域为止 .
4.3 GC收集器
4.3.1 Serial收集器
- Serial收集器是最基本且发展最悠久的收集器 , 在JDK1.3.1之前 , 它是JVM新生代收集的唯一选择
- 它是一个单线程收集器 , 这里的单线程不仅仅说明它只会使用一个CPU或者一条线程进行GC .
- 它在进行GC时 , 必须中断其他所有的工作线程 , 直到垃圾收集过程结束 .
- 由于这些过程都是由虚拟机在后台自动发起和自由完成的 , 整个过程不可控 , 所以这个收集器在实际使用中不被接受 .
- 到现在为止 , 它仍是虚拟机运行在client模式下的默认的新生代收集器 . 毕竟介于其单线程的特性 , 对于单个CPU的环境来说 , 没有线程之间交互的消耗 , 其相对要简单而高效很多 .
收集算法 :
- 新生代采用复制算法
- 老年代采用标记-整理算法
- 都会暂停所有用户线程 .
4.3.2 ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本 , 除了使用多条线程进行垃圾回收之外 , 其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等
控制参数如有 :
- -XX:SurvivorRatio: 新生代Eden/survivor的内存比例(默认8:1:1)
- -XX:PretenureSizeThreshold
- -XX:HandlePromotionFaiilure
- ParNew除了本身的多线程收集之外 , 其他与Serial并无太多创新之处, .但它却是许多运行在Server模式下的虚拟机中的首选的新生代收集器 .
- 目前除了Serial收集器之外 , 只有ParNew收集器可以与CMS收集器配合工作 .
- 控制参数 :
- 使用-XX:UseConcMarkSweepGC后为默认新生代收集器
- 强指定 : -XX:UseParNewGC
4.3.3 Parallel Scavenge收集器
- 一个新生代收集器
- 使用复制算法的收集器
- 并行的多线程收集器
- Parallel Scavenge收集器的最大关注点在于 : 达到一个可控制的吞吐量(Throughput) .
- 吞吐量 : CPU运行用户代码的时间与CPU总消耗时间的比值 . 即 : 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 控制参数:
- -XX:MaxGCPauseMillis : 控制最大垃圾收集停顿时间 , (> 0 milliSeconds) , 停顿时间与吞吐量和新生代空间密切相关 , 缩小最大收集停顿时间 , 某种程度上将会牺牲收集器的吞吐量和JVM新生代的空间 .
- 例如要缩短停顿时间 : 可以调小新生代空间大小 , 这样当然会加速收集时间, 但会导致GC更加频繁 , 降低吞吐量
- -XX:GCTimeRatio: 直接设置吞吐量 . (0 < 吞吐量 < 100). 默认值为99, 即最大允许 1% 的时间进行垃圾收集
- -XX:UseAdaptiveSizePolicy: 一个开关参数 , 即自适应调整策略
- 当该参数打开时 , 不在需要指定-Xxm(新生代的大小) 、 和 -XX:Survivor(Eden()与Servivor(存货区)的比例)以及-XX:PrementenureSizeThreshold(晋升老年代对象的大小)等细节参数 . 虚拟机会根据当前运行情况动态调整这些参数以提供最大吞吐量或者最合适停顿时间 . 这种方式成为GC的自适应调节策略(GC Ergonomics)
- 当使用自适应调整策略时 , 只需要把-Xmx(最大堆内存)设置好 , 后面使用-XX:MaxGCPauseMillis(最大垃圾收集停顿时间)或-XX:GCTimeRatio(吞吐量)给虚拟机设立一个自动调整的优化目标即可 .
- 老年代收集 : 该收集器架构中默认使用PS MarkSweep收集器进行老年代收集
4.3.4 Serial Old收集器
- Serial收集器的老年代版本
- 同样也是一个单线程收集器
- 使用标记-整理算法进行收集
- 主要意义给client模式下的虚拟机使用
- server模式下有两大用途 :
- 在JDK1.5及之前的版本中可与Parallel Scavenge收集器搭配使用
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
- 注 : 在Parallel Scavenge收集中的老年代收集器PS MarkSweep收集器的实现方式与Serial Old大致相近, 所以在在1.5及之前的JDK版本中都可以使用Serial Old收集器代替 .
4.3.5 Parallel Old收集器
- Parallel Scavenge收集器的老年代版本
- 使用多线程+标记-整理算法
- JDK1.6之后提供
- 为解决使用Parallel Scavenge收集时只能使用Serial Old(或PS MarkSweep)收集器的问题,由于使用Serial Old收集器在服务端整体性能上的拖累,使用Parallel Scavenge收集器也未必能达到整体应用上最大吞吐量的结果
4.3.6 CMS收集器
- CMS: (Concurrent Mark Sweep)收集器
- 以获取最短回收停顿时间(Stop The World的时间)为目标,多应用于互联网站或者B/S架构系统的服务器上
- Concurrent : 是指垃圾回收的GC线程和执行用户程序的线程是可以并发执行的
- 基于标记-清除算法实现,整个过程为 :
- 初始标记(CMS initial mark)
- 并发标记(CMS Concurrent Mark)
- 并发预先清除(CMS Preclean)
- 并发可能失败的预先清除(Concurrent Abortable Preclean)
- 重新标记(CMS remark)
- 并发清除(CMS Concurrent Sweep)
- 并发重置(Concurrent Reset)
- 注意 :
- 标记:会将存活的对象和要回收的对象都标记出来
- 清除: 清除要回收的对象 . 即进行对象回收
- 其中 :
- 初始标记和重新标记时需Stop The World . 即仍需停掉其他所有线程
- 并发标记阶段并不会影响执行用户代码的线程的执行 . 因此才会有重新标记 .
- 重新标记是为了修正并发标记期间因用户程序继续执行而导致标记产生变动的这一部分的记录标记
- 在整个过程中, 除初始标记和重新标记外 , 并发标记和并发清除都不会触发stop the world . 即都可以与执行用户代码的线程并发执行 .
- 优点 :
- 支持并发收集 . 低停顿时间(指的是停止用户线程的时间低) .
- 缺点:
- CMS收集器对CPU资源比较敏感 .
- 无法处理浮动垃圾 , 可能会出现Concurrent Mode Failure失败而触发另一次Full GC.
- 浮动垃圾: 即在初始标记时对象被判断为存活的对象 , 在清除之前 , 因为用户线程是并行执行的,可能有些线程已经执行完毕 , 有些对象变成了需要回收 , 这部分本该在本次GC过程中进行回收但是没有回收的对象称之为浮动垃圾
- 如果程序中老年代增长不是很快 , 可适当调高: -XX:CMSInitiatingOccupancyFration的值来提高触发百分比
以降低内存回收次数 . 进而提高整体性能 .
- 或者通过-XX:UseSerial Old启动Serial Old回收器进行老年代的回收
- -XX:CMSInitiatingOccupancyFration设置太高容易导致大量的并发收集失败.反而影响性能
- 基于并发-清除的算法 , 收集结束时会有大量空间碎片产生 , 会给大内存对象的内存分配带来麻烦 .
4.3.7 G1收集器
- 并发收集器,也称之为垃圾优先收集器
- 设计初衷 :
- 为了尽量缩短处理超大堆(大于4G)时产生的停顿 , 相对于CMS而言产生内存碎片的概率大大降低 .
- 设计原则
- 简单可行的性能调优
- 取消了新生代和老年代的物理空间划分
- G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
- 在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。
- 常用命令 :
- -XX:UseG1GC : 使用G1收集器
- -XX:MaxGCPauseMillis=200:设置GC的最大停顿时间为200ms
若有收获,就点个赞吧