一、什么是垃圾
没有任何引用指向的一个或多个对象(循环引用)
申请内存方式 | 释放内存方式 | |
---|---|---|
C | malloc | free(手动) |
C++ | new | delete(手动) |
java | new | 自动回收 |
自动回收内存,编程上简单,系统不容易出错。手动释放内存,容易出现两种类型的问题:
1、忘记回收(拿C来说,使用了malloc,忘记使用free)
2、多次回收(回收后又多次回收)
二、如何定位垃圾
1、引用计数(reference count)
在一个对象上引用一个数字(记录有多少个引用指向它),每减少一个引用,数字就减1,当没有引用指向这个对象时(此时数字为0),这个对象就成为垃圾了
引用计数存在的问题:不能解决循环引用的问题
如下图:三个对象循环引用,但是没有其他引用指向它们三种的任意一个,这三个对象实际上是3个垃圾;根据引用计数(因为它们三循环引用,三个对象计数都为1),不会将这三个对象定义为垃圾,最后导致的问题,内存泄漏
2、根可达算法(Root Searching)—根搜索算法
通过程序找到跟对象,通过跟对象找到与它相连接(引用)的对象,
根对象(GC Roots):
三、常见的垃圾回收算法
1、标记清除(Mark-Sweep)
缺点:位置不连续,产生碎片
2、复制(拷贝)算法(Copying)
优点:没有碎片
缺点:空间利用率低(浪费内存)
3、标记压缩(Mark-Compact)
GC标记-压缩算法有两个阶段:标记阶段和压缩阶段。
其中标记阶段和GC标记-清除算法的标记阶段完全相同。在压缩阶段中,将堆中的活跃对象按顺序移动到堆的一侧,消除对象之间的空闲区域,起到压缩的作用。
优缺点:没有碎片,效率比拷贝算法略低
4、JVM分代算法
1.新生代(new | young)
2.老年代(old)
3.永久代(JDK1.7)、元数据区(Metaspace)(JDK1.8)
永久代和元数据区的区别:
①都装的是Class对象
②永久代必须指定大小限制,元数据区可以设置,也可以不设置,无上限(受限于物理内存)
③字符串常量1.7存在永久代,1.8则是在堆中
新生代 = Eden区 + 2个survivor(幸存的意思)区
为什么有两个survivor区?方便内存(垃圾)的回收
第一次YGC回收之后,大多数的对象会被回收,活着的进入到survivor0区域
YGC再次回收,Eden区活着的对象 + survivor0活着的对象全部copy到survivor1区域,Eden区和survivor0区域清空
四、垃圾回收器
图中红线代表的是垃圾回收器之间能相互组合使用
1、Serial
2、Parallel Scavenge
3、ParNew
4、SerialOld 将Serial的单线程算法放在Old区
5、ParallelOld 将Parallel的多线程算法放在Old区
6、ConcurrentMarkSweep(CMS)
并发——在垃圾回收线程运行时,应用程序能正常的运行,降低STW的时间(官方200ms以内)
STW(Stop The Word)指的是在GC回收的过程中,应用程序所有线程会停止(给用户的感觉就是系统卡顿),等GC回收完毕之后,应用程序才继续运行,中途卡顿的时间称为STW
gc回收的过程中,为什么会设计STW这个概念,如果不用这个概念行不行?
不行,如果不要STW,会使得在GC运行过程中可能又有很多垃圾(也可能不是垃圾 被误GC线程认为垃圾)的对象,这样使得GC线程不会停止…(大致是这样,说不清了…)
7、G1,STW时间控制在10ms
8、ZGC,STW时间控制在1ms
9、Shenandoah(跟ZGC都很强,竞争)
10、Eplison
JKD1.8默认的垃圾回收器:Parallel Scavenge + ParallelOld
五、Jvm调优
JVM参数分类:
标准参数:-开头,所有的HostSpot都支持
非标准参数:-X开头,特定版本HostSpot支持特定命令
不稳定参数:-XX开头,下个版本可能会取消
常用例子:
-XX:+PrintFlagsFinal 设置值(最终生效值)
-XX:+PrintFlagsInitial 默认值
-XX:+PrintCommandLineFlags 命令行参数(可以查看程勋运行使用了哪些参数)
eg:在cmd窗口运行 java -XX:+PrintCommandLineFlags
1、大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如∶字符串、数组)。JVM参数-XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效。
比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC,再执行下上面的第一个程序会发现大对象直接进了老年代
为什么要这样呢?
为了避免为大对象分配内存时的复制操作而降低效率。
2、长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor
空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程
度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-
XX:MaxTenuringThreshold来设置。
3、对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小
的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代
了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时
就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象
动态年龄判断机制一般是在minor gc之后触发的。
4、Minor gc后存活的对象Survivor区放不下
这种情况会把存活的对象部分挪到老年代,部分可能还会放在Survivor区
5、老年代空间分配担保机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个"-XX:-HandlePromotionFailure"(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发ful gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM"