概述
垃圾回收
常见的垃圾回收算法
引用计数
- 缺点:
- 每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗。
- 较难处理循环引用
- JVM的实现一般不采用这种方式
复制
- Java堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区和To Survivor区)和老年呆。
- 优点:没有产生内存碎片(因为整体复制)
- 缺点:相对浪费空间,耗时
MinorGC的过程(复制->清空->互换)
- eden、SurvivorFrom复制到SurvivorTo,年龄+1
- 当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收
- 经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经到达了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
- 清空edeb、SurvivorFrom
- 清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to
- SurvivorTo和SurvivorFrom互换
- SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。
- 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
标记清除
- 垃圾收集算法-标记清楚法(Mark-Sweep):算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。
- 优点:未对对象大面积的复制,节约了内存空间
- 缺点:产生内存碎片
标记整理
- 标记-压缩(Mark-Compact)
- 原理:
- 标记(Mark):与标记-清除一样。
- 压缩(Compact):再次扫描,并往一端滑动存活对象。
- 优点:没有内存碎片,可以利用bump
- 缺点:需要移动对象的成本
GC Roots
如何判断一个对象是否可以被回收
-
引用计数法
- Java中,引用和对象是有关联的。如果要操作对象则必须引用进行。
- 因此,简单的办法是通过引用计数来判断一个对象是否可以回收。即给对象中添加一个引用计数,每当有一个引用失效时,计数器值减1.
- 任何时刻计数器值为0的对象就是不可能再被利用的,那么这个对象就是可回收对象。
- 主流的Java虚拟机里面都没有选择这种算法是它很难解决对象之间相互循环引用的问题。
-
枚举根节点做可达性分析(根搜索路径)
- 为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
- 所谓“GC roots”或者tracing GC的“根集合”就是一组必须活跃的引用。
- 基本思路就是通过一系列名为“GC Roots”的对象作为起点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可达性的)对象就被判定为存活,没有被遍历到的就被判断为死亡。
可以作为GC Roots的对象
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象。
四种主要垃圾收集器类型
串行垃圾回收器(Serial)
- Serial(新生代)、SerialOld(老年代)
- 它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
并行垃圾回收器(Parallel)
- Parallal(新生代)、ParNew(新生代)、Parallal(老年代)
- 多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理平台处理等弱交互场景。
并发垃圾回收器(CMS)
- ConcMarkSweep(老年代)
- 用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,使用对响应时间有要求的场景。
- 回收步骤
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 并发回收:和用户线程一起交替执行
GI垃圾回收器
-
以前收集器特点
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden+S0+s进行复制算法
- 老年代收集必须扫描整个老年代区域
- 都是以尽可能少而快速地执行GC为设计原则。
-
G1(Garbage-First)收集器,是一款面向服务端应用的收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外还具有一下特性:
- 像CMS收集器一样,能与应用程序线程并发执行。
- 整理空间空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐性能
- 不需要更大的Java Heap
-
G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
-
底层原理
- G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器
- Region区域化垃圾收集器,G1将新生代、老年代的物理空间取消了。
- 区域化内存划片Region,整体编为一系列不连续的内存区域,避免了全内存的GC操作。
- 在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可
- 每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。
- 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB-32MB,且必须是2的幂),默认将整个堆划分为2048个分区。大小范围在1MB-32MB,最多能设置2048个区域。也即能够支持的最大内存为:32MB*2048=65536MB=64G内存。
- 核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小。
-
Region包含的类型
- 新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
- 老年代,G1收集器通过将对象从一个区域复制到另一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
- Humongous(巨大的)区域。如果一个对象占用的空间超过了分区容量的50%以上,,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
-
回收步骤
- G1收集器下的Young GC
-
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片。
-
Eden区的数据移动到新的Survivor区,部分数据晋升到Old区。
-
Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区。
-
最后Eden区收集干净了,GC结束,用户的应用程序继续执行。
-
- G1收集器下的Young GC
-
4步过程
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程
- 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化的回收。
-
常用配置参数
- -XX:G1HeapRegionSize=n
- 设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域。
- -XX:MaxGCPauseMillis=n
- 最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
- -XX:InitiatingHeapOccupancyPercent=n
- 堆占用了多少的时候就触发GC,默认为45
- -XX:ConcGCThreads=n
- 并发GC使用的线程数。
- -XX:G1ReservePercent=n
- 设置作为空闲的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
- -XX:G1HeapRegionSize=n
-
和CMS相比的优势
- G1不会产生内存碎片
- 是可以精确控制停顿。该收集器是把整个堆(新生代、老生代)划分成多个固定大小的区域,每次根据允许停顿时间去手机垃圾最多的区域。
开启不同垃圾回收器的命令
- +XX:UseSerialGC:Serial(新生代)+SerialOld(老年代)
- +XX:UseParNewGC:ParNew(新生代)+SerialOld(老年代)
- +XX:UseParallalGC:Parallal(新生代)+ParallalOld(老年代)
- +XX:UseParallalOldGC:Parallal(新生代)+ParallalOld(老年代)
- +XX:UseConcMarkSweepGC:Parallal(新生代)+ CMS(老年代)+SerialOld(逃生门)
- +XX:UseG1GC:G1(新、老)
六种OOM
java.lang.StackOverFlowError
- 解决办法
- 把虚拟机栈变大:
-Xss1024k
- 把虚拟机栈变大:
java.lang.OutOfMemoryError:Java heap space
- 解决办法
- 把堆内存变大:
-Xmx2g -Xms2g
- 把堆内存变大:
java.langOutOfMemoryError:Metaspace
- 解决办法
- 把元空间内存变大:
-XX:MetaSpaceSize=256m -XX:MaxMetaSpaceSize=512m
- 把元空间内存变大:
java.lang.OutOfMemoryError:unable to create new native thread
- 该native thread异常与对应的平台有关
- 高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError:unbale to create new native thread
- 导致原因:
- 应用创建了太对线程,一个应用进程创建多个线程,超过系统承载极限。
- 服务器并不允许应用程序创建那么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,如果应用创建超过这个数量,就会报java.lang.OutOfMemoryError:unable to create new native thread
- 解决办法:
- 想办法降低应用程序创建线程的数量,分析应用是否真的需要创建那么多线程,如果不是,改代码将线程数降到最低。
- 对于有的应用,确实需要创建多个线程,远超过linux系统默认的1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认限制。
java.lang.OutOfMemoryError:Direct buffer memory
-
导致原因:
- 写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在java堆和Native堆中来回复制数据。
- ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
- ByteBuffer.allocateDirect(capability)第一种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝,所以速度相对较快
- 但如果不断分配内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
-
解决办法
- 把本地内存变大:
+XX:MaxDirectMemorySize=1g
- 把本地内存变大:
java.lang.OutOfMemoryError:GC overhead limit exceeded
-
GC回收时间长时会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。
-
假设不抛出GC overhead limit错误,会发生GC清理的内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC缺没有任何成果。
最后
以上就是风趣吐司为你收集整理的JVM垃圾回收与六种OOM垃圾回收六种OOM的全部内容,希望文章能够帮你解决JVM垃圾回收与六种OOM垃圾回收六种OOM所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复