写JAVA程序,一定要了解JVM(JAVA Virtual machine)一些基础知识和垃圾收集。如果对JVM已经很了解了,可以不用继续往下阅读了。本文只针对Java 7, 后续版本的可能跟本文会有所差异。接下来咱们先看一张图:
Java虚拟机分为堆,栈,永久区,程序计数器,虚拟机栈,本地方法栈咱们先从比较重要的地方开始:
堆: 是所有对象生成存放的地方,它包含新生代和老年代,是所有对象保存的地方。通过制定参数-XX:Xms堆的最小值,-XX:Xmx堆的最大值(图片上写成了Xmn,请见谅)。
新生代:新生代分为三个区域,Eden区,和两个相同大小的区域Survivor0和Survivor1。生成的地方是Eden伊甸园,万物开始的地方,经过一段时间之后Eden区开始满了,垃圾回收后存活的对象需要向S0或者S1去倒入,咱们可以想象成堆是一个大水缸,当里边的水满出来了,就需要流入到另一个罐子里边去。那么流入罐子的时候要进行的是Minor GC,在这个时候需要STW(Stop the world),Minor GC会耗费很短的STW时间以致于基本程序感知不到。S0和S1是两个相同大小的池子,其中一个池子一直是空的。如果其中的一直池子满了也会进行Minor GC。可以通过设置-XX:Xmn来设置年轻代的大小,可以通过-XX:SurvivorRatio来设置Eden和S0或S1的比例(也就是Eden:Survivor0:Survivor1的比例,默认为8:1:1),比如:新生代为10M,设置-XX:SurvivorRatio=2(2:1)的话, Eden区为5M,S0+S1=5M,每个大小为2.5M。
老年代:当对象经过多次垃圾回收后仍然存活的话就会移动到老年代。可以通过参数-XX:NewRatio可以设置新生代与老年代的比例,比如-XX:NewRatio=2则新生代的大小占堆空间为1/3,老年代占堆空间的2/3。老年代的GC为Major GC,一般Minor GC发生的时候,网老年代里去倒入数据的时候,发现老年代的空间不足了,所以需要进行GC,那么在这是如果老年代也没有空间了,需要进行Full GC。
永久区 :说到永久区包括方法区存储着类的结构信息,字符串池(只限Java 7),运行时常量池(静态方法,运行时常量),Java SE包的信息和方法。如下图所示。
程序计数寄存器:是当前线程字节码的行号指示器,字节码解释器就是通过更改它然后程序进行分支,循环,跳转以及异常处理。也就是它是线程私有的,当程序正在执行Java方法的时候,那么当前寄存器里边存储的就是执行字节码的行号。如果执行Native方法则计数器的值为Null。
虚拟机栈:就是程序执行的时候存储的局部变量原始数据类型和引用变量的地址,这个也是线程私有的,每个线程独立的,这块可以通过-Xss来设置其大小。如果进行递归调用时可能
本地方法栈:与虚拟机栈类似,但是它是在执行一些native方法时存储相应的变量。具体native的方法可能是一些其他语言写的方法,不同的操作系统可能不一样。
下面聊一下垃圾回收:
串行垃圾回收(-XX:+UserSerialGC):这种方式采用单线程垃圾回收机制,同时使用标记,压缩方法进行垃圾回收,一般用于清除年轻代和老年代的Minor GC和Major GC,这种一般适用于单核CPU的,并且资源消耗比较小的机器上,一般嵌入到工具里。
并行垃圾回收(-XX:+UseParallelGC):这种方式跟单线程的比,它采用了多线程对年轻代进行垃圾收集(标记-压缩)。可以用过参数-XX:ParallelThreads=n来设置它的线程数,但是对于老年代还是采用单线程的方式进行垃圾收集。这种收集器适用于短时进行很多小的计算,最后进行汇总,然后可以接收长时间的程序停止。比如向一些批处理产生报表,账单,大量数据库查询等。
并行老年代回收(-XX:+UseParallelOldGC):采用多线程对年轻年轻代进行垃圾回收的同时(年轻代采用复制算法),也用多线程对老年代进行垃圾回收(老年代采用标记-压缩算法)相比并行垃圾回收,应用停止时间会快一些。
并发标记清除回收(-XX:+UseConcMarkSwapGC) : 并发标记清除(如下图片来自网络,串行标记清除收集器和并发标记清除收集器区别),很适合应用程序低延迟,尤其需要反映很快的应用程序。它的优点是在很短的时间进行STW并且他的STW可以进行定制。多个线程进行标记和清除任务,同时部分用户的线程在某个阶段不会停止,这就是为什么它STW时间很短。采用初始标记(STW,回收线程参考黄色线)ClassLoader能查找到的最近的存活对象进行标记;然后并发的进行标记,被存活对象能找到的对象的引用对象进行再次标记,同时其他用户线程(蓝色部分)也在执行程序;重新标记,STW然后检查那些之前标记的对象进行修正,比如其新添加的对象和它没有引用的对象;最后将并发地去清除这些没有引用的对象。
它的优点很显然,但是它的缺点也是有的,比如它需要更多的CPU和内存去进行运算,默认情况下,它会启动(CPU个数+3)/3个线程去进行回收,CPU很少的话可能会适得其反。
多线程在进行清理的时候,同时也会产生其他的垃圾对象没法进行立即回收。
它更容易产生碎片,因为没有进行压缩,所以大对象处理比较麻烦,比如一个大对象因为老年代没有连续的空间去存放这个大对象,那么就需要更大的内存空间,那么需要至少4G的空间去处理,看到如此多的缺点,需要这么大的空间,新的收集算法产生了,那就是G1,下面继续说G1。
G1收集器-Garbage First Collector(-XX:UseG1GC),Java 7才推出的。这种收集器将Eden, Survivor, Old Generation分为不同的区域块中。它采用了并发,并行,增量压缩,低延迟,可预测STW时间的收集器。推荐使用的一种垃圾回收算法,相对比较复杂。它和CMS类似,但是它还采用了复制算法和压缩算法。G1是Java 7推荐的算法,一般使用在6G内存中。
和CMS比较类似,但是也有些区别,CMS不压缩,而它采用压缩。G1并发地标记在整个堆里的存活的对象,当完成标记之后,G1知道哪些区域标记清理之后几乎为空,所以先对哪块区域进行清理,让出大量的空间,这也就是为什么Garbage First了。G1采用了转移,将一部分区域,移动并压缩,放到另一块区域里边,以便腾出更多的空间来使用,并且采用并发地处理,减少停顿时间。
G1的老年代收集基本流程为初始化标记,STW,然后将年轻代标记存活下来的对象进行标记。并发标记,将那些存活的对象进行标记,此时用户线程是正在执行的。重新标记,对引用对象进行重新标记,并且清除空的区域,腾出新的空间并且计算出可用空间。复制/清除阶段,将那些很少用到的区域进行最先收集,进行老年代和年轻代垃圾清理。此时会产生STW。清理后阶段,将那些很少用到的区域进行压缩和复制,以腾出更多的空间进行内存分配。
参考:
http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html