深入JVM虚拟机(三) Java GC垃圾收集
Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题。
1、引用计数法:
引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
缺点:
- 引用和去引用伴随加法和减法,影响性能
- 很难处理循环引用
图中3个对象引用值都为1,它们都不可回收。
注:在JAVA中未使用引用计数法。
2、标记清除法:
标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
标记出存活对象,将未标记的垃圾对象全部清降,或者标记出拉圾对象,将拉圾对象全部清除。
3、标记压缩法:
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
4、复制算法:
优势:与标记-清除算法相比,复制算法是一种相对高效的回收方法。不适用于存活对象较多的场合如老年代。
原理:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
缺点:空间浪费
优化:整合标记清理思想:
- 将大对象复制到担保空间(保留空间)回收垃圾几次后,将大对象放到老年代。左侧的表格中将小对象复制到右侧表格中空闲空间。最后清空原来使用的空间。
5、分代思想:
依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法:
- 少量对象存活,适合复制算法。
- 大量对象存活,适合标记清理或者标记压缩。
1、可触及性:
可触及的:从根节点开始进行扫描,可以触及到这个对象,那么这个对象就是可触及的。
可复活的:一旦所有引用被释放,就是可复活状态,因为在finalize()中可能复活该对象。
不可触及的:在finalize()后,可能会进入不可触及状态,不可触及的对象不可能复活,可以回收。
JAVA代码:
public class CanReliveObj {
public static CanReliveObj obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanReliveObj finalize called");
// GC垃圾回收器,只会调用一次finalize(),obj赋值当前对象,变成了可触及状态
obj = this;
}
@Override
public String toString() {
return "I am CanReliveObj";
}
public static void main(String[] args) throws InterruptedException {
// 声明对象
obj = new CanReliveObj();
// 将对象赋值给null,一般赋值为null,垃圾回收器,将回收值为null的对象
obj = null; // 可复活
// 调用gc()方法,调用对象的finalize()方法,此时obj赋值this
System.gc();
// 当前线程睡眠1秒
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
/*
* 由于finalize()方法只会在调用gc()的时候调用一次,
* 调用gc()方法过后,不会再调用finalize()方法,此时对象为null。
*/
System.out.println("第二次gc");
obj = null; // 不可复活
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
}
}
经验:
避免使用finalize(),操作不慎可能导致错误。
优先级低,何时被调用,不确定,何时发生GC不确定。
可以使用try-catch-finally来替代它。
2、根:
- 栈中引用的对象
- 方法区中静态成员或者常量引用的对象(全局对象)
- JNI方法栈中引用对象
产生Stop The World的原因:
Java中一种全局暂停的现象,所有的线程全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互,多半由于GC引起。
当GC开始工作时,将现在在进行的线程全部都停止,以保证不会再有先的垃圾产生。如果不能暂定正在进行线程,垃圾清理的清况就无法得到保证(Sun将这件事情称为“Stop The World”)。
影响:
长时间服务停止,没有响应。
遇到HA系统,可能引起主备切换,严重危害生产环境。
Java代码:
public class PrintThread extends Thread{
public static final long starttime=System.currentTimeMillis();
@Override
public void run(){
try{
while(true){
long t=System.currentTimeMillis()-starttime;
System.out.println("time:"+t);
Thread.sleep(100);
}
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
PrintThread printThread = new PrintThread();
printThread.start();
}
}
预期,应该是每秒中有10条输出:
执行结果 |
GC垃圾回收日志 |
time:2018 time:2121 time:2221 time:2325 time:2425 time:2527 time:2631 time:2731 time:2834 time:2935 time:3035 time:3153 time:3504 time:4218 time:4349 time:4450 time:4551 |
3.292: [GC3.292: [DefNew: 959K->63K(960K), 0.0024260 secs] 523578K->523298K(524224K), 0.0024879 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 3.296: [GC3.296: [DefNew: 959K->959K(960K), 0.0000123 secs]3.296: [Tenured: 523235K->523263K(523264K), 0.2820915 secs] 524195K->523870K(524224K), [Perm : 147K->147K(12288K)], 0.2821730 secs] [Times: user=0.26 sys=0.00, real=0.28 secs] 3.579: [Full GC3.579: [Tenured: 523263K->523263K(523264K), 0.2846036 secs] 524159K->524042K(524224K), [Perm : 147K->147K(12288K)], 0.2846745 secs] [Times: user=0.28 sys=0.00, real=0.28 secs] 3.863: [Full GC3.863: [Tenured: 523263K->515818K(523264K), 0.4282780 secs] 524042K->515818K(524224K), [Perm : 147K->147K(12288K)], 0.4283353 secs] [Times: user=0.42 sys=0.00, real=0.43 secs] 4.293: [GC4.293: [DefNew: 896K->64K(960K), 0.0017584 secs] 516716K->516554K(524224K), 0.0018346 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] ……省略若干….. 4.345: [GC4.345: [DefNew: 960K->960K(960K), 0.0000156 secs]4.345: [Tenured: 522929K->12436K(523264K), 0.0781624 secs] 523889K->12436K(524224K), [Perm : 147K->147K(12288K)], 0.0782611 secs] [Times: user=0.08 sys=0.00, real=0.08 secs] |
红色加粗的地方是GC引起的线程停顿(Stop The World)现象。垃圾回收的时间基本上是等于停顿的时间。
在Eclipse中设置eclipse.ini文件。
3、打开eclipse安装的根目录下%{Eclipse_HOME}\eclipse.ini,在文件的末尾添加参数:
-XX:+PrintGC
-verbose:gc
-Xloggc:../logs/jvm-gc/gc.log
4、PrintGC打印GC的简要信息
[GC 4790K->374K(15872K), 0.0001606 secs]
[GC 4790K->374K(15872K), 0.0001474 secs]
[GC 4790K->374K(15872K), 0.0001563 secs]
[GC 4790K->374K(15872K), 0.0001682 secs]
GC之前使用:4790K
GC之后使用:374K
整个堆的大小:15872K
1、打开eclipse安装的根目录下%{Eclipse_HOME}\eclipse.ini,在文件的末尾添加参数:
-XX:+PrintHeapAtGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-verbose:gc
-Xloggc:../logs/jvm-gc/gc.log
2、PrintGCDetails打印详信息,PrintGCTimeStamps打印时间戳,如:
[GC[DefNew: 4416K->0K(4928K), 0.0001897secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00,real=0.00 secs]
GC之前使用:4416K
GC之后使用:0
整个堆的大小:4928K
3、PrintHeapAtGC打印详信息
程序运行结束后会将整个堆的运行状态,打进行印:
Heap
def new generation total13824K, used 11223K [0x27e80000,0x28d80000,0x28d80000)
eden space12288K, 91% used [0x27e80000, 0x28975f20, 0x28a80000)
from space1536K, 0% used [0x28a80000, 0x28a80000, 0x28c00000)
to space1536K, 0% used [0x28c00000,0x28c00000, 0x28d80000)
tenured generation total5120K, used 0K [0x28d80000, 0x29280000, 0x34680000)
the space 5120K, 0%used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000)
compacting perm gen total 12288K,used 142K [0x34680000, 0x35280000, 0x38680000)
the space 12288K, 1%used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000)
ro space 10240K, 44%used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
rw space 12288K, 52% used [0x39080000,0x396cdd28, 0x396cde00, 0x39c80000)
新生代def new generation空间共:total 13824K
已经使用:used 11223K
低边界:0x27e80000
当前边界:0x28d80000
最高边界:0x28d80000
新生代内存:(0x28d80000-0x27e80000)/1024/1024=15M
新生代总合:(12288K+1536K+1536K) /1024=15M
新生代可申请内存:13824K = 12288K + 1536K
生成对象eden空间:space12288K,使用量为91%
新生代from to两个值是相等的。
老年代tenured generation空间共:total 5120K
已经使用:used 0k
方法区compacting perm gen共total 12288K
已经使用:used 142K
-Xmx参数:最大堆
-Xms参数:最小堆
1、-Xmx1024 –Xms256m
运行代码:
public static void main(String[] args) {
System.out.print("Xmx=");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("freemem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("totalmem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
}
运行结果:
Xmx=1811.5M
freemem=120.04971313476562M
total mem=122.0M
2、堆分配参数总结:
- 根据实际事情调整新生代和幸存代的大小。
- 官方推存新生代占堆的3/8
- 幸存代占新生代的1/10
3、内存参数总结:
参数名称 |
含义 |
默认值 |
说明 |
-Xms |
初始堆大小 |
物理内存的1/64(<1GB) |
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. |
-Xmx |
最大堆大小 |
物理内存的1/4(<1GB) |
默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 |
-Xmn |
年轻代大小(1.4or lator) |
|
注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 |
|
|
|
整个堆大小=年轻代大小 +年老代大小 +持久代大小. |
|
|
|
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 |
-XX:NewSize |
设置年轻代大小(for 1.3/1.4) |
|
|
-XX:MaxNewSize |
年轻代最大值(for 1.3/1.4) |
|
|
-XX:PermSize |
设置持久代(perm gen)初始值 |
物理内存的1/64 |
|
-XX:MaxPermSize |
设置持久代最大值 |
物理内存的1/4 |
|
-Xss |
每个线程的堆栈大小 |
|
JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 |
|
|
|
一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长) |
|
|
|
和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"” |
|
|
|
-Xss is translated in a VM flag named ThreadStackSize” |
|
|
|
一般设置这个值就可以了。 |
-XX:ThreadStackSize |
Thread Stack Size |
|
(0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.] |
-XX:NewRatio |
年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) |
|
-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 |
|
|
|
Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 |
-XX:SurvivorRatio |
Eden区与Survivor区的大小比值 |
|
设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 |
-XX:LargePageSizeInBytes |
内存页的大小不可设置过大, 会影响Perm的大小 |
|
=128m |
-XX:+UseFastAccessorMethods |
原始类型的快速优化 |
|
|
-XX:+DisableExplicitGC |
关闭System.gc() |
|
这个参数需要严格的测试 |
-XX:MaxTenuringThreshold |
垃圾最大年龄 |
|
如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代.对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率 |
|
|
|
该参数只有在串行GC时才有效. |
-XX:+AggressiveOpts |
加快编译 |
|
|
-XX:+UseBiasedLocking |
锁机制的性能改善 |
|
|
-Xnoclassgc |
禁用垃圾回收 |
|
|
-XX:SoftRefLRUPolicyMSPerMB |
每兆堆空闲空间中SoftReference的存活时间 |
1s |
softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap |
-XX:PretenureSizeThreshold |
对象超过多大是直接在旧生代分配 |
0 |
单位字节 新生代采用Parallel Scavenge GC时无效 |
|
|
|
另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. |
-XX:TLABWasteTargetPercent |
TLAB占eden区的百分比 |
1% |
|
-XX:+CollectGen0First |
FullGC时是否先YGC |
FALSE |
|
4、辅助信息
-XX:+PrintGC |
|
输出形式: |
-XX:+PrintGCDetails |
|
输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs] |
|
|
|
-XX:+PrintGCTimeStamps |
|
|
-XX:+PrintGC:PrintGCTimeStamps |
|
可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 |
-XX:+PrintGCApplicationStoppedTime |
打印垃圾回收期间程序暂停的时间.可与上面混合使用 |
输出形式:Total time for which application threads were stopped: 0.0468229 seconds |
-XX:+PrintGCApplicationConcurrentTime |
打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 |
输出形式:Application time: 0.5291524 seconds |
-XX:+PrintHeapAtGC |
打印GC前后的详细堆栈信息 |
|
-Xloggc:filename |
把相关日志信息记录到文件以便分析. |
|
|
与上面几个配合使用 |
|
-XX:+PrintClassHistogram |
garbage collects before printing the histogram. |
|
-XX:+PrintTLAB |
查看TLAB空间的使用情况 |
|
XX:+PrintTenuringDistribution |
查看每次minor GC后新的存活周期的阈值 |
|
|
|
|
-XX:+PrintTLAB |
查看TLAB空间的使用情况 |
|
XX:+PrintTenuringDistribution |
查看每次minor GC后新的存活周期的阈值 |
Desired survivor size 1048576 bytes, new threshold 7 (max 15) |
|
|
new threshold 7即标识新的存活周期的阈值为7。 |
--以上为《深入JVM虚拟机(三) Java GC垃圾收集》,如有不当之处请指出,我后续逐步完善更正,大家共同提高。谢谢大家对我的关注。
——厚积薄发(yuanxw)