学习笔记-理解GC

  java中将内存的控制交给JVM来实现,在JVM的内存模型中,堆内存是JAVA内存区域中最大的一部分,GC主要就是发生在堆中,用来回收那些无用的对象。我们自己想一想,肯定是没用的对象需要被回收,那么如何判断哪些对象还有用,哪些没用了呢?一个对象被创建,如果被引用了,那这个对象肯定是有用的,如果引用全失效了,那就是没用的对象了,需要被回收。基于这个思想,引用计数法出现了。引用计数算法:这个非常容易理解,给每个对象添加一个引用计数器,对象每被引用一次,引用计数器就+1,引用失效时就-1。那么判断一个对象是否有用的条件就变成了对这个计数器值得判断了,如果为0,那么被回收,如果为>0,那么保留。但是这种方式会产生一个问题,就是对象之间的循环引用无法被识别,即使这两个对象不能被访问,但是它们之间互相引用着对方,故而计数器肯定>0,那么就不能被回收。JVM中并没有使用引用计数算法,而是使用了根搜索算法。

根搜索算法:这个算法也不难理解,通过条件,选择一系列的对象成为“GC Roots"对象,然后将”GC Roots"对象作为起始点开始向下搜索,搜索所有走过的路径成为“引用链”。在这个引用链上的对象就保留,而如果一个或多个互相引用的对象不在这个引用链上,或者说对象到“GC Roots"不可达,那么这些就是无用的对象,都需要被回收。

  在java语言中,可作为GC Roots的对象包括下面几种:1) 虚拟机栈(栈帧中的本地变量表)中引用的对象

                          2) 方法区中类静态属性引用的对象

                          3) 方法区中常量引用的对象

                          4) 本地方法栈中JNI(即一般说的Native方法)引用的对象

  我们知道了什么样的对象会被GC了吧,那么JVM又是通过什么方式来回收这些内存的呢?

  标记-清除算法:试着想一想,如果要你要设计一个算法清除满足收集条件的对象来释放内存的时候你该怎么做呢?最简单的是不是就是把需要回收的对象标记一下,然后直接全部回收就行了?照着这个思路就是”标记-清除算法”的思想了,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。想法很简单,实际也就是这么做的。但是呢,这种方式是不是最好的?有什么缺陷?想到这里,就需要分析一下了。一个个的标记然后清除,效率高吗?当然不。看看下图的标记-清除算法的示意图,可以发现,标记-清除之后会产生大量的内存碎片,如果碎片太多,当程序运行没有足够连续的内存空间来存放大对象的时候,就会不得不提前触发一次GC。概括来说就是有两个缺点:效率不高;内存碎片可能导致提前发生GC。

 

 

  学习算法的应该都很清楚,效率是很重要的,有时候需要使用空间来换时间提高效率,那么就需要了解一下第二种回收算法了——复制算法,它的思想就是空间换时间,将内存容量划分成相等的两块,当这一块的内存用完了,就将还存活的内存复制到另一块上,然后再把使用过的内存空间一次性清理干净。这样每次都是对其中的一块的内存进行回收,也就不需要考虑内存碎片等复杂情况了,只需要移动堆顶指针,然后按照顺序分配即可,实现简单,运行高效。但是缺点也很明显:内存变成一半了.下图就是复制算法的示意图:

 

 

  我们知道,在JVM中堆内存的新生代(new )中的对象存活率较低,采用复制算法每次需要复制的对象也不是很多,效率较高,空间换时间值得的。现在的商业虚拟机都是采用复制算法来回收新生代,新生代中对象98%是朝生夕死,所以并不需要按照1:1的比例来划分空间来实现复制算法,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一个Survivor空间。当发生GC的时候,将Eden空间和Survivor空间中还存活的对象拷贝到另一个没使用的Survivor空间中,然后再清理掉Eden和刚刚使用的Survivor空间。Hotspot虚拟机默认Eden和Survivor的大小比例是8:1,也就是新生代每次可以使用的内存空间是整个新生代的90%,只有10%的空间会被浪费。我们知道了在JVM中对于新生带的垃圾回收使用的复制算法(此时发生的GC成为young gc),效率高,我们也就只牺牲了10%的内存空间,挺不错的。请注意这里提到的young gc,后面会提到full gc。但是虽然一般情况下有98%的对象是朝生夕死,需要回收的,但是不能保证每次回收的时候对象的存活率都低于10%,一旦超过了10%,那么空闲的survivor空间就不够用了,此时就必须依赖老年代的空间来进行分配担保(就相当于A找B借钱,C替A做担保,保证如果A换不起就自己来还,C就是担保人,映射到内存中老年代所占内存就是担保人)。如果空闲的Survivor空间无法存放上次GC之后的存活对象,那么这些对象就会通过分配担保机制进入老年代。老年代呢,里面保存的都是生存周期较长的对象(老年代里面的对象都是经过了新生代,然后多次存活下来的对象),而复制算法在应对这种存活率极高的内存区域的对象回收时,需要执行较多的复制操作,效率将会变低。关键的还是如果不想浪费50%的空间,那么就需要分配担保机制(参考新生代的设计),但是并没有额外的空间来担保了。所以对于老年代的特性,有人提出了一种“标记-整理算法”,看到这里肯定就想到了前面提到的“标记-清除算法“了,OK,这两个算法标记的过程都是一样的,就在于”标记-整理算法”不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,示意图如下图所示。很明显,这种”标记-整理算法“的效率不高,所以如果老年代发生GC,那么效率也就不高了,并且一旦老年代发生GC,那么发生的必然是Full GC ,Full GC 会同时对老年代和新生代进行GC操作,顺便也会回收一下perm gen中的内存,所以相比较young gc来说很慢,我们在JVM调优的时候需要避免JVM频繁发生full gc。full gc的速度比young gc要慢10倍。

  分代收集算法:  通过上述的分析呢,就知道了对于堆中的新生代和老年代会采用不同的垃圾回收算法来回收“死亡”的对象,这种分代回收对象的方法称为“分代收集算法”。这个分代收集算法根据各个年代的特点采用适当的收集算法。在新生代中,每次GC的时候都发现大批的对象死去,只有少量存活,自然选用复制算法;而对于老年代这种存活率高、没有额外担保空间的,就必须使用“标记-清除算法”或者“标记-整理算法“了。 GC设计的理论基础就是这些了,其实原理还是比较容易理解的。GC的具体实现就是垃圾收集器,目前尚没有一个垃圾收集器是完美的,需要配合使用。下面插上一副堆内存划分图。

8.20更新

常用的jvm参数

 

verbose:gc表示,启动jvm的时候,输出jvm里面的gc信息。Full GC 就表示执行了一次Full GC的操作,178K 和99K 就表示执行GC前内存容量和执行GC后的内存容量。1984K就表示内存总容量。后面那个是执行本次GC所消耗的时间,单位是秒。格式如下:

[Full GC 178K->99K(1984K), 0.0253877 secs]

 

-XX:+printGC 这个打印的GC信息跟上个一样

 

-XX:+PrintGCDetails 打印GC的详细信息。new generation 就是堆内存里面的新生代。total的意思就是一共的,所以后面跟的就是新生代一共的内存大小。used也就是使用了多少内存大小。0x开头的那三个分别代表的是 底边界,当前边界,高边界。也就是新生代这片内存的起始点,当前使用到的地方和最大的内存地点,eden space 这个通常被翻译成伊甸园区,是在新生代里面的,一些创建的对象都会先被放进这里。后面那个12288K就表示伊甸园区一共的内存大小,91% used,很明显,表示已经使用了百分之多少。后面的那个0x跟上一行的解释一样。from space 和to space 是幸存者的两个区。也是属于新生代的。他两个区的大小必须是一样的。因为新生代的GC采用的是复制算法,每次只会用到一个幸存区,当一个幸存区满了的时候,把还是活的对象复制到另个幸存区,上个直接清空。这样做就不会产生内存碎片了。tenured generation 就表示老年代。compacting perm 表示永久代。格式如下:

–Heap
– def new generation   total 13824K, used 11223K [0x27e80000, 0x28d80000, 0x28d80000)
–  eden space 12288K,  91% used [0x27e80000, 0x28975f20, 0x28a80000)
–  from space 1536K,   0% used [0x28a80000, 0x28a80000, 0x28c00000)
–  to   space 1536K,   0% used [0x28c00000, 0x28c00000, 0x28d80000)
– tenured generation   total 5120K, 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)

  

-XX:+PrintGCTimeStamps 打印GC发生的时间戳。 289.556表示从jvm启动到发生垃圾回收所经历的的时间。GC表示这是新生代GC(Minor GC)。PSYoungGen表示新生代使用的是多线程垃圾回收器Parallel Scavenge。314113K->15937K(300928K)]这个跟上面那个GC格式一样,只不过,这个是表示的是新生代,幸存者区。后面那个是整个堆的大小,GC前和GC后的情况。Times这个显而易见,代表GC的所消耗的时间,用户垃圾回收的时间和系统消耗的时间和最终真实的消耗时间。

289.556: [GC [PSYoungGen: 314113K->15937K(300928K)] 405513K->107901K(407680K), 0.0178568 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

293.271: [GC [PSYoungGen: 300865K->6577K(310720K)] 392829K->108873K(417472K), 0.0176464 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

  

-X:loggc:log/gc.log  这个就表示,指定输出gc.log的文件位置。

 

-XX:+PrintHeapAtGC  表示每次GC后,都打印堆的信息。

 

-XX:+TraceClassLoading  监控类的加载。使用这个参数就能很清楚的看到那些类被加载的情况了。

•[Loaded java.lang.Object from shared objects file]
•[Loaded java.io.Serializable from shared objects file]
•[Loaded java.lang.Comparable from shared objects file]
•[Loaded java.lang.CharSequence from shared objects file]
•[Loaded java.lang.String from shared objects file]
•[Loaded java.lang.reflect.GenericDeclaration from shared objects file]
•[Loaded java.lang.reflect.Type from shared objects file]

  

-XX:+PrintClassHistogram  跟踪参数。这个按下Ctrl+Break后,就会打印一下信息:

–分别显示:序号、实例数量、总大小、类型。

这里面那个类型,B和C的其实就是byte和char类型。

num     #instances         #bytes  class name
  
----------------------------------------------
  
   1:        890617      470266000  [B
  
   2:        890643       21375432  java.util.HashMap$Node
  
   3:        890608       14249728  java.lang.Long
  
   4:            13        8389712  [Ljava.util.HashMap$Node;
  
   5:          2062371680  [C
  
   6:           46341904  java.lang.Class

  

-Xmx -Xms 这个就表示设置堆内存的最大值和最小值。这个设置了最大值和最小值后,jvm启动后,并不会直接让堆内存就扩大到指定的最大数值。而是会先开辟指定的最小堆内存,如果经过数次GC后,还不能,满足程序的运行,才会逐渐的扩容堆的大小,但也不是直接扩大到最大内存。  

Xmx是整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m。堆的不同分布情况,对系统会产生一定的影响。尽可能将对象预留在新生代,减少老年代GC的次数(通常老年回收起来比较慢)。实际工作中,通常将堆的初始值和最大值设置相等,这样可以减少程序运行时进行的垃圾回收次数和空间扩展,从而提高程序性能。

而Xms 此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

 

-Xmn 设置新生代的内存大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。一般在增大年轻代内存后,也会将会减小年老代大小。

 

-XX:NewRatio  新生代包括Eden和两个Survivor区)和老年代的比例。比如:1:4,就是新生代占五分之一。

 

-XX:SurvivorRatio  设置两个Survivor区和eden区的比例。比如:2:8 ,就是一个Survivor区占十分之一。

 

-XX:+HeapDumpOnOutMemoryError  发生OOM时,导出堆的信息到文件。

 

-XX:+HeapDumpPath  表示,导出堆信息的文件路径。

 

-XX:OnOutOfMemoryError  当系统产生OOM时,执行一个指定的脚本,这个脚本可以是任意功能的。比如生成当前线程的dump文件,或者是发送邮件和重启系统。

 

-XX:PermSize -XX:MaxPermSize  设置永久区的内存大小和最大值。永久区内存用光也会导致OOM的发生。

 

-Xss  设置栈的大小。栈都是每个线程独有一个,所有一般都是几百k的大小。 更具应用的线程所需内存大小进行调整。

 

posted @ 2016-08-25 13:51  小杨ABC  阅读(264)  评论(0编辑  收藏  举报