java虚拟机在执行java程序的过程中会把他所管理的内存划分为若干个不同的数据区域,运行时数据区分别为:程序计数器,java虚拟机栈,本地方法栈,java堆,方法区。还有一个非运行时数据区:直接内存(Direct Memory)。 1)程序计数器 每个线程创建的时候会有一个独立的程序计数器,可以看成是线程执行的字节码的行号指示器,不同线程之间的程序计数器互不影响,存储于程序计数器所在的内存中,这块内存很小。线程私有 2)java虚拟机栈 每个方法被执行的时候虚拟机都会创建一个栈帧,用于存储局部变量表, 操作数栈,动态链接,方法出口等信息,一个方法被调用至结束,就对应着栈帧在虚拟机栈中从入栈到出栈的过程。线程私有 局部变量表存放了编译器可知的各种基本数据类型:boolean,byte,char,short,int,float,long,double,对象引用类型和returnAddress类型。对象引用类型可以是一个指向对象起始地址的指针,也可以是指向一个代表对象的句柄。returnAddress类型指向一条字节码指令地址。 3)本地方法栈 调用native方法所需要用的栈 4)java堆 java堆是虚拟机所管理的内存中最大的区域,在java虚拟机启动的时候创建。几乎所有的对象实例以及数组都在堆中分配。堆是垃圾收集器管理的主要对象,又称“GC堆”,如果要细分的话,java堆还可以分为Eden,From Survivor,To Survivor等。线程共享 5)方法区 方法区用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译的代码等,方法区又称“非堆”,用于与堆区分。线程共享,对于方法区的内存回收主要是对常量池的回收以及对对象的卸载。运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量以及符号引用。 直接内存(Direct Memory)并不是java虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,它可以使用native函数库直接分配对外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,避免了java堆与native堆中来回复制数据。 垃圾收集器的种类有:Serial收集器,ParNew收集器,Parallel Scavenge收集器,Serial Old收集器,Parallel Old收集器,CMS收集器,G1收集器 1)Serial收集器 单线程,工作时需要暂停用户所有线程,可以和CMS配合工作 2)ParNew收集器 多线程,工作时需要用户暂停所有线程,可以和CMS配合工作 3)Parallel Scavenge收集器 Parallel Scavenge收集器的主要目标是达到可控制吞吐量。吞吐量就是CPU运行用户代码的时间占CPU消耗时间的比值,即吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集的时间)。-XX:MaxGCPauseMillis=M设置垃圾收集时间尽量在M毫秒完成,减低M值不一定可以提高吞吐量;-XX:GCTimeRatio=N用于设置允许最大的垃圾收集时间占总时间的1/(1+N),默认值为99 4)Serial Old收集器 Serial Old是老年代版本,可以与Parallel Scavenge收集器配合使用,或者作为CMS的后备预案 5)Parallel Old收集器 Parallel Old收集器是Parallel Scavenge收集器的老年代版本,Parallel Scavenge收集器不可以与CMS收集器配合使用,如果使用Serial Old收集器,性能会被拖累,所以可以选择Parallel Old收集器 6)CMS收集器 CMS收集器(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS的工作步骤:初始标记,并发标记,重新标记,并发删除。在初始标记以及重新标记的时候需要暂停用户所有线程,但是耗时非常短。初始标记就是标记直接与GC ROOT关联的对象,并发标记就是标记从GC ROOT可以连通的对象,重新标记就是在并发标记的过程中因为用户线程继续运行导致标记变更的对象的记录,最后并发清除未被标记的对象。 CMS收集器的优点:并发收集,低停顿 CMS收集器的缺点:CMS对CPU比较敏感,默认线程数为(CPU数量+3)/4,无法处理浮动垃圾(垃圾回收过程中用户线程没有停止导致),标记清楚算法会产生空间碎片 7)G1收集器 G1收集器采用标记整理算法,没有碎片;可以精确控制停顿,指定M毫秒的时间片段内,消耗在垃圾搜集上的时间不能超过N毫秒;可以在不牺牲吞吐量的情况下完成低停顿的垃圾回收,原理:把堆分成多个大小固定的独立区域,并跟踪垃圾的堆积程度,在后台维护一个优先级列表 垃圾收集器采用的算法: 1)标记清除算法 会产生大量的空间碎片 2)复制算法 减小了内存,把内存分成2分,一份保持完整,当另一份存储不下的时候启用 3)标记整理算法 4)分代搜集 把堆分成新生代以及老年代,新生代使用复制算法,老年代使用标记整理算法 java虚拟机没有使用引用计数算法,而是使用根搜索算法来查找不可用对象。 引用计数算法:当一个对象被引用,引用计数器加1,当引用失效的时候,引用计数器减1,当引用计数器为0的时候,被认为是可收回对象。 根搜索算法:通过一系列的”GC ROOTS“对象作为起始点,从这些点向下搜索,不能喝GC ROOTS联通的对象便是可回收对象 java中可以作为“GC ROOTS”的对象包括:虚拟机栈(栈帧中的本地变量表)中的引用对象,方法区中的类静态属性引用的对象,方法区中的常量引用对象,本地方法栈中JNI(Native方法)的引用对象。 引用的类型: 强引用:只要强引用还存在,GC永远不会回收内存 软引用:对于软引用关联着的对象,当要发生内存溢出异常之前,把这些对象列入回收范围并进行第二次回收 弱引用:对于弱引用关联着的对象,能下一次GC工作的时候,不管内存是否足够,都会被回收 虚引用:虚引用不会影响关联着的对象的生存时间,也无法通过虚引用来获取对象的实例,虚引用的唯一目的就是这个对象被GC回收时收到一个系统通知 垃圾收集器的执行逻辑: 1)用根搜索算法进行第一次标记 2)对标记的对象进行一次筛选,如果对象覆盖了finalize方法,并且没有执行过,那么有可能在这个finalize方法中再次使用这个对象,这样的话就把该对象从待删除的对象队列中删除。finalize方法只能执行一次,保证对象循环引用的时候不可能永远存在。 3)java虚拟机创建一个低优先级的Finalize线程去执行对象覆盖的finalize方法,垃圾收集器对待删除队列再次进行标记 4)垃圾收集器把两次标记的对象回收了 =========================================================================== 下面我们来看下GC日志: ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 8 * 9 * @param args 10 */ 11 public static void main(String[] args) { 12 // TODO Auto-generated method stub 13 byte[] alloc1, alloc2, alloc3, alloc4; 14 alloc1 = new byte[2 * _1M]; 15 alloc2 = new byte[2 * _1M]; 16 alloc3 = new byte[2 * _1M]; 17 // alloc4 = new byte[4 * _1M];//发生Minor GC 18 } 19 } 上面代码运行以后得到下面的日志: -verbose:gc 打印垃圾收集器的执行日志 -Xms20M 最小堆的大小 -Xmx20M 最大堆的大小 -Xmn10M 年轻代的大小 -XX:SurvivorRatio=8 设置Eden与Survivor的大小比例,默认8:1 以上参数得知该程序跑的虚拟机为堆的大小为20M,年轻代为10M,老年代10M,年轻代中Eden=8M,From Survivor = To Survivor = 1M, alloc1,alloc2,alloc3总共6M,Eden区域足够容纳,不触犯Minor GC(年轻代垃圾回收)。 下面把alloc4注释去掉运行: DefNew是指垃圾收集器的类型,6487K->148K指Minor GC前后年轻代的大小,9216为年轻代的总大小,6487K->6292K指Minor GC前后jvm堆的大小,19456指jvm堆的总大小,0.0041845 secs指Minor GC耗时,user=0.00指用户耗时,sys=0.0指系统耗时,real=0.00指Minor GC实际耗时。def new generation total 9216K,used 4408K指年轻代堆的大小为9216K,使用了4408K;其中Eden区占了8192K,使用了52%(alloc4所占的内存),from space(From Survivor,使用了14%)以及to space(to Survivor)各占1024K;tenured generation total 10240K,used 6144K指的是老年代的大小为10240K,用了6144K(alloc1,alloc2,alloc3),使用60%。 alloc1,alloc2,alloc3执行以后Eden区就有6M,最大只有8M,当alloc4=4M想要放入的时候,Eden区大小不够,所以触发Minor GC,发现alloc1,alloc2,alloc3都是存活对象,有发现Survivor的空间只有1M,不够大,所以利用空间分配担保机制,把alloc1,alloc2,alloc3存放入老年代,老年代占用60%,alloc4就放入了Eden区。 下面的2张图可以很好的解释各个参数(原址:http://blog.csdn.net/alivetime/article/details/6895537): Minor GC: Full/Marjor GC: 配置参数-XX:PretenureSizeThreshold=3M,可以使大于3M的对象直接进入老年代 如果Survivor空间相同年龄的对象的大小总和大于Survivor空间的一半,那么大于等于该年龄的对象就直接进入老年代 空间分配担保: 每次Minor GC执行完以后,虚拟机会检查之前晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则发起一次Full/Major GC,如果小于,则查看HandlePromotionFailure值是否允许担保失败,如果允许,那只会进行一次Minor GC。如果不允许失败,那么也要改为进行一次Full/Major GC 之前晋升老年代的平均大小大于老年代的剩余空间大小 ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 8 * 9 * @param args 10 */ 11 public static void main(String[] args) { 12 // TODO Auto-generated method stub 13 byte[] alloc1, alloc2, alloc3, alloc4, alloc5; 14 alloc1 = new byte[2 * _1M]; 15 alloc2 = new byte[2 * _1M]; 16 alloc3 = new byte[2 * _1M]; 17 alloc4 = new byte[4 * _1M];// 发生Minor GC 18 alloc2 = null; 19 alloc3 = null; 20 alloc4 = null; 21 alloc5 = new byte[6 * _1M]; 22 } 23 } 日志如下: [GC [DefNew: 6487K->148K(9216K), 0.0049804 secs] 6487K->6292K(19456K), 0.0050137 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 这段代码总共发生了2次GC,alloc4 = new byte[4 * _1M];这个时候发生一次,因为Eden区内存不够用了,这次内存回收会把Eden区的所有的存活的对象转移到老年代,并把alloc4存放入Eden区,此时Eden区4M,Survivor区0M,老年区6M;第二次GC发生在最后一个alloc1 = new byte[6 * _1M],Eden区占有4M,不够用,所以Eden区的存活对象要进入老年代,老年代之前晋升了6M,大于剩余空间2M,所以此时发生一次Full/Major GC,因为alloc2,alloc3,alloc4设置为null,所以在这次Full/Major GC中被清理了,老年代只剩下2M,新的alloc1=6M进入Eden区。 从日志中可以看到我的分析结果:Eden区占用了77%,大约6M,tenured generation占21%大约2M,为什么不是精确的相等,我的理解是垃圾收集器线程运行本身会耗费一定量的资源,所以不会严格的相等。 之前晋升老年代的平均大小小于老年代的剩余空间大小,并且关闭空间担保机制-XX:-HandlePromotionFailure,但是我在机器上跑出来的日志显示,java虚拟机并没有进行一次Full/Major GC!!!下面是我的代码: ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails 8 * -XX:-HandlePromotionFailure 9 * 10 * @param args 11 */ 12 public static void main(String[] args) { 13 // TODO Auto-generated method stub 14 byte[] alloc1, alloc2, alloc3, alloc4, alloc5; 15 alloc1 = new byte[1 * _1M]; 16 alloc2 = new byte[1 * _1M]; 17 alloc3 = new byte[1 * _1M]; 18 alloc4 = new byte[6 * _1M];// 发生Minor GC 19 alloc2 = null; 20 alloc3 = null; 21 alloc1 = null; 22 alloc5 = new byte[5 * _1M]; 23 } 24 } GC日志: 代码分析: alloc1,alloc2,alloc3 分别分配1M,当alloc4=6M的时候Eden区不够大,并且Survivor区也不够大,直接将alloc1,alloc2,alloc3分配入老年代,alloc4分配入Eden区,alloc1,alloc2,alloc3设置为null,alloc5=5M的时候Eden区已经存有6M的alloc4,所以不够大,要存入老年代,首先判断之前晋升入老年代的平均大小3M小于老年代的剩余空间7M,又因为我关闭了空间担保机制,所以应该会出现一次Full/Major GC,但是从日志来看并没有出现Full/Major GC,还是只进行了一次Minor GC,这里不理解,哪位大牛知道请指导下,万分感谢。继续摸索!!看下面的2个代码以及日志的比较,应该就是问题的答案了。 看看下面代码,我们是否查询到一些端倪 ![]() 1 public class Test2 { 2 private static int _1M = 1024 * 1024; 3 4 /** 5 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails 6 * -XX:-HandlePromotionFailure 7 * 8 * @param args 9 */ 10 public static void main(String[] args) { 11 // TODO Auto-generated method stub 12 byte[] alloc1, alloc2, alloc3, alloc4, alloc5,alloc6; 13 alloc1 = new byte[1 * _1M]; 14 alloc2 = new byte[1 * _1M]; 15 alloc3 = new byte[1 * _1M]; 16 // alloc6 = new byte[1 * _1M]; 17 alloc4 = new byte[6 * _1M];// 发生Minor GC 18 alloc1 = null; 19 alloc2 = null; 20 alloc3 = null; 21 alloc4 = null; 22 alloc5 = new byte[5 * _1M]; 23 } 24 } gc日志: [GC [DefNew: 3415K->148K(9216K), 0.0025138 secs] 3415K->3220K(19456K), 0.0025390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 把alloc6的注释去掉,执行以后的gc日志为: [GC [DefNew: 4439K->148K(9216K), 0.0032273 secs] 4439K->4244K(19456K), 0.0032624 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 通过以上2个代码以及日志的比较是不是可以得出结论: 当之前的每次晋升到老年代的平均大小小于老年代的剩余空间大小的时候,如果设置不允许担保失败,并且老年代的剩余空间大小不足以存放本次晋升的对象的大小,那么会触发一次Full/Major GC,如果老年代的剩余空间大小足以存放本次晋升的对象的大小,那么就会执行Minor GC。 ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails 8 * -XX:-HandlePromotionFailure 9 * 10 * @param args 11 */ 12 public static void main(String[] args) { 13 // TODO Auto-generated method stub 14 byte[] alloc1, alloc2, alloc3, alloc4, alloc5; 15 alloc1 = new byte[2 * _1M]; 16 alloc2 = new byte[2 * _1M]; 17 alloc3 = new byte[2 * _1M]; 18 alloc4 = new byte[6 * _1M];// 发生Minor GC 19 alloc5 = new byte[2 * _1M]; 20 } 21 } 日志: 不管有没有启用担保失败机制,最后的2M内存都会被分配入老年代。 想知道空间担保机制做运作,所以想先把空间担保机制关闭,如果之前晋升老年代的平均大小小于老年代的剩余空间大小,那么会直接发生Full/Major GC,如果把空间担保机制开启,那么就不会发生Full/Major GC,会直接执行一次Minor GC。 如果有问题或者哪里写的不对的地方,请各位大牛斧正,不胜感谢! jvm参数优化参考:http://unixboy./blog/174173 扬帆起航,生命正式从这里开始... 爱情终将消失于茫茫的时间洪流之中,沉淀于厚重的黄泥沙丘之下... |
|