JVM调优可从GC和分代堆内存方面入手
GC(Garbage Collection)基本算法:
标记-清除 Mark-Sweep:分垃圾标记阶段和内存释放阶段。标记阶段,找到所有可访问对象打个标记。清理阶段,遍历整个堆,对未标记对象(即不再使用的对象)清理。
Mark-Sweep最大的问题会造成内存碎片,但是效率很高,不浪费空间。
标记-压缩 (压实)Mark-Compact:分垃圾标记阶段和内存整理阶段。标记阶段,找到所有可访问对象打个标记。内存清理阶段时,整理时将对象向内存一端移动,整理后存活对象连续的集中在内存一端。
Mark-Compact算法好处是整理后内存空间连续分配,有大段的连续内存可分配,没有内存碎片。缺点是内存整理过程有消耗,效率相对低下
复制 Copying:先将可用内存分为大小相同两块区域A和B,每次只用其中一块,比如A。当A用完后,则将A中存活的对象复制到B。复制到B的时候连续的使用内存,最后将A一次性清除干净。
缺点是比较浪费内存,只能使用原来一半内存,因为内存对半划分了,复制过程毕竟也是有代价。
好处是没有碎片,复制过程中保证对象使用连续空间。
GC算法总结:
没有最好的算法,在不同场景选择最合适的算法
效率:复制算法>标记清除算法> 标记压缩算法
内存整齐度:复制算法=标记压缩算法> 标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
堆内存分代
将heap内存空间分为三个不同类别: 年轻代、老年代、持久代
永久代:JDK1.7之前使用, 即Method Area方法区,保存JVM自身的类和方法,存储JAVA运行时的环境信息,JDK1.8后 改名为 MetaSpace,此空间不存在垃圾回收,关闭JVM会释放此区域内存,此空间物理上不属于heap内存,但逻辑上存在于heap内存
- 永久代必须指定大小限制,字符串常量JDK1.7存放在永久代,1.8后存放在heap中
- MetaSpace 可以设置,也可不设置,无上限
年轻代回收 Minor GC:
1. 起始时,所有新建对象(特大对象直接进入老年代)都出生在eden,当eden满了,启动GC。这个称为Young GC 或者 Minor GC。
2. 先标记eden存活对象,然后将存活对象复制到s0(假设本次是s0,也可以是s1,它们可以调换),eden剩余所有空间都清空。GC完成。
3. 继续新建对象,当eden满了,启动GC。
4. 先标记eden和s0中存活对象,然后将存活对象复制到s1。将eden和s0清空,此次GC完成
5. 继续新建对象,当eden满了,启动GC。
6. 先标记eden和s1中存活对象,然后将存活对象复制到s0。将eden和s1清空,此次GC完成
以后就重复上面的步骤。
通常场景下,大多数对象都不会存活很久,而且创建活动非常多,新生代就需要频繁垃圾回收。但是,如果一个对象一直存活,它最后就在from、to来回复制,如果from区中对象复制次数达到阈值(默认15次,CMS为6次,可通过java的选项 -XX:MaxTenuringThreshold=N 指定),就直接复制到老年代。
老年代回收 Major GC
进入老年代的数据较少,所以老年代区被占满的速度较慢,所以垃圾回收也不频繁。
如果老年代也满了,会触发老年代GC,称为Old GC或者 Major GC。
由于老年代对象一般来说存活次数较长,所以较常采用标记-压缩算法。
当老年代满时,会触发 Full GC,即对所有"代"的内存进行垃圾回收
Minor GC比较频繁,Major GC较少。但一般Major GC时,由于老年代对象也可以引用新生代对象,所以先进行一次Minor GC,然后在Major GC会提高效率。可以认为回收老年代的时候完成了一次Full GC。
所以可以认为 MajorGC = FullGC
Full GC 触发条件:
- 老年代满了
- System.gc()手动调用。不推荐
年轻代:
- 存活时长低
- 适合复制算法
老年代:
- 区域大,存活时长高
- 适合标记清除和标记压缩算法