1. 谁需要GC
栈:不需要 ,不是共享的对象
堆:需要
方法区/元空间:也需要。 元空间(独立于JVM的内存空间,只受限于系统本身的内存)
2. GC 的一般参数
-Xms 堆区内存初始内存分配的大小
-Xmx 堆区内存可被分配的最大上限
打印GC详情
-XX:+PrintGCDetails
当堆内存空间溢出时输出堆的内存快照
-XX:+HeapDumpOnOutOfMemoryError
堆中参数配置:
新生代大小: -Xmn20m 表示新生代大小20m(初始和最大)
-XX:SurvivorRatio=8 表示Eden和Survivor(2)的比值,
缺省为8 表示 Eden:From:To= 8:1:1
2 Eden:From:To= 2:1:1
3. GC如何判断对象的存活
可达性分析:
在Java, 可作为GC Roots的对象包括:
方法区: 类静态属性的对象;
方法区: 常量的对象;
虚拟机栈(本地变量表)中的对象.
本地方法栈JNI(Native方法)中的对象。
public class GCRootTest {
Object o = new Object();
static Object gcRootTest1 = new Object();
final static Object getGcRootTest2 = new Object();
public static void main(String[] args) {
Object obj1 = gcRootTest1; //可达 = 在对象中是引用,传递右侧对象的地址
Object obj2 = obj1; //可达
Object obj3 = obj2; //可达
}
public void test() {
//不可达 (方法运行完后回收)
Object obj4 = o;
Object obj5 = obj4;
Object obj6 = obj5;
}
//本地变量表中引用的对象
public void stack() {
Object obj7 = new Object(); //new 出对象,属于虚拟机栈中本地变量表的对象
Object obj8 = obj7;
// obj8 在方法(运行完)出栈前,都是可达的。 (跟test方法,似乎也没有区别)
}
4. 引用类型
-
强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器不会回收它,哪怕因内存不足,引发了OutOfMemoryError -
软引用(SoftReference)
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存。
public static void main(String[] args) {
Man man = new Man("peter");
SoftReference<Man> sorfMan = new SoftReference<>(man);
man = null;
System.out.println(sorfMan.get());
System.gc();
System.out.println("-------gc ------");
System.out.println(sorfMan.get());
List<byte[]> list = new LinkedList<>();
try {
for (int i = 0; i < 1000; i ++) {
System.out.println("----------" + sorfMan.get());
list.add(new byte[1024 * 1024]);
}
} catch (Throwable e) {
System.out.println("Exception ------" + e.getMessage() + ", sorfMan " + sorfMan.get());
}
}
Man name peter
[GC (System.gc()) [PSYoungGen: 1723K->496K(2560K)] 1723K->576K(9728K), 0.0009803 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 80K->472K(7168K)] 576K->472K(9728K), [Metaspace: 3150K->3150K(1056768K)], 0.0049414 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
-------gc ------
Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
----------Man name peter
[GC (Allocation Failure) --[PSYoungGen: 1162K->1162K(2560K)] 7779K->7779K(9728K), 0.0007110 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1162K->1032K(2560K)] [ParOldGen: 6616K->6603K(7168K)] 7779K->7636K(9728K), [Metaspace: 3237K->3237K(1056768K)], 0.0048426 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) --[PSYoungGen: 1032K->1032K(2560K)] 7636K->7636K(9728K), 0.0004065 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 1032K->1031K(2560K)] [ParOldGen: 6603K->6587K(7168K)] 7636K->7618K(9728K), [Metaspace: 3237K->3237K(1056768K)], 0.0047505 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid53056.hprof ...
Heap dump file created [8589024 bytes in 0.017 secs]
Exception ------Java heap space, sorfMan null
Heap
PSYoungGen total 2560K, used 1167K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 2048K, 56% used [0x00000007bfd00000,0x00000007bfe23c50,0x00000007bff00000)
from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 7168K, used 6587K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
object space 7168K, 91% used [0x00000007bf600000,0x00000007bfc6ec08,0x00000007bfd00000)
Metaspace used 3286K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
- 弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
public static void main(String[] args) {
Man man = new Man("peter");
WeakReference<Man> weakMan = new WeakReference<>(man);
man = null;
System.out.println(weakMan.get());
System.gc();
System.out.println("-------gc ------");
System.out.println(weakMan.get());
}
Man name peter
-------gc ------
null
Process finished with exit code 0
- 虚引用(PhantomReference)
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
应用场景:虚引用主要用来跟踪对象被垃圾回收器回收的活动。
5. 回收算法
5. 1复制算法(Copying)
image.png将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
注意这里还是涉及到了内存复制,如果每次不能回收的内存较多,这样效率会降低很多。
-
特点
实现简单、运行高效
内存复制、没有内存碎片
利用率只有一半 -
关于新生代
- 空间担保(Handle Promotion)
新生代内存不够时,会放老年代,即老年代是新生代的空间担保。 - 内存分配比例(8:1:1)
新生代大部分对象,会很快会回收掉,”朝生夕死“。复制算法,会预留两个交换区,分别是 From Survivor ,To Survivor ,比例基本为1:1。占用大概20%空间,相应的Eden 占用80%。
5. 2 标记-清除算法(Mark-Sweep)
image.png算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
- 特点
利用率百分之百
不需要内存复制
有内存碎片
5. 3 标记-整理算法(Mark-Compact)
image.png首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 特点
利用率百分之百
没有内存碎片
需要内存复制
效率一般
5.4 堆内存分配策略对象优先在Eden分配
-
特点
大对象直接进入老年代
长期存活的对象将进入老年代
动态对象年龄判定
空间分配担保 -
GC的触发条件(空间不够了)
新生代 Minor GC
老年代 Full GC
对象优先在Eden分配,如果说Eden内存空间不足,就会发生Minor GC。
大对象:大对象直接进入老年代,需要大量连续内存空间的Java对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。
-XX:PretenureSizeThreshold 参数 ,大于这个数量直接在老年代分配,缺省为0 ,表示绝不会直接分配在老年代。
长期存活的对象:将进入老年代,默认15岁(反复复制15次,每次年龄+1)
-XX:MaxTenuringThreshold调整。
动态对象年龄判定:为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保:新生代中有大量的对象存活,survivor空间不够,当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行Minor GC,否则FullGC。
6. JVM的具体策略 --- 分代收集
image.png image.png image.png命令行参数查看:jps -v
image.png
并行:垃圾收集的多线程的同时进行。
并发:垃圾收集的多线程和应用的多线程同时进行。
注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)
垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
细节
-
Serial/Serial Old
最古老的,单线程,独占式,成熟,适合单CPU 服务器
-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old -
ParNew
和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old -
Parallel Scavenge(ParallerGC)/Parallel Old
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。 -
Concurrent Mark Sweep (CMS)
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:
初始标记-短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记-和用户的应用程序同时进行,进行GC RootsTracing的过程
重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
-XX:+UseConcMarkSweepGC ,表示新生代使用ParNew,老年代的用CMS
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
G1
image.png
-XX:+UseG1GC
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
内存布局:在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
ν 新生代GC
回收Eden区和survivor区,回收后,所有eden区被清空,存在一个survivor区保存了部分数据。老年代区域会增多,因为部分新生代的对象会晋升到老年代。
ν 并发标记周期
初始标记:短暂,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,产生一个全局停顿,都伴随有一次新生代的GC。
根区域扫描:扫描survivor区可以直接到达的老年代区域。
并发标记阶段:扫描和查找整个堆的存活对象,并标记。
重新标记:会产生全局停顿,对并发标记阶段的结果进行修正。
独占清理:会产生全局停顿,对GC回收比例进行排序,供混合收集阶段使用
并发清理:识别并清理完全空闲的区域,并发进行
ν 混合收集
对含有垃圾比例较高的Region进行回收。
G1当出现内存不足的的情况,也可能进行的FullGC回收。
G1中重要的参数:
-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。
-XX:ParallerGCThreads:设置GC的工作线程数量Stop The World现象
GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。
7 举个栗子
ArrayList 源码中,remove 方法,会调用fastRemove 方法
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
注意上面的 // clear to let GC do its work
数组中存储的元素,属于GC Roots的 对象 。remove 方法执行完,不会被GC 回收,所以这里手动释放了。
网友评论