JVM调优总结(八)

Posted on

JVM调优总结(八)-典型配置举例2

常见配置汇总

堆设置

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小

收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParalledlOldGC:设置并行年老代收集器

-XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

调优总结

年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

  1. 并发垃圾收集信息

  2. 持久代并发收集次数

  3. 传统GC信息

  4. 花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

1. **-XX:+UseCMSCompactAtFullCollection:**使用并发收集器时,开启对年老代的压缩。

2. **-XX:CMSFullGCsBeforeCompaction=0:**上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

JVM内存管理:深入Java内存区域与OOM

Posted on

JVM内存管理:深入Java内存区域与OOM - 高级语言虚拟机

您还未登录 ! 登录 注册

ITeye3.0

群组首页编程语言高级语言虚拟机知识库JVM基础JVM内存管理:深入Java内存区域与OOM 原创作者: IcyFenix 阅读:10325次 评论:5条 更新时间:2011-05-26

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

概述:

对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。

对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。

VM运行时数据区域

JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据《Java虚拟机规范(第二版)》(下文称VM Spec)的规定,JVM包括下列几个运行时数据区域:

1.程序计数器(Program Counter Register):

每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,对于非Native方法,这个区域记录的是正在执行的VM原语的地址,如果正在执行的是Natvie方法,这个区域则为空(undefined)。此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。

2.Java虚拟机栈(Java Virtual Machine Stacks)

与程序计数器一样,VM栈的生命周期也是与线程相同。VM栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。在后文中,我们将着重讨论VM栈中本地变量表部分。

经常有人把Java内存简单的区分为堆内存(Heap)和栈内存(Stack),实际中的区域远比这种观点复杂,这样划分只是说明与变量定义密切相关的内存区域是这两块。其中所指的“堆”后面会专门描述,而所指的“栈”就是VM栈中各个帧的本地变量表部分。本地变量表存放了编译期可知的各种标量类型(boolean、byte、char、short、int、float、long、double)、对象引用(不是对象本身,仅仅是一个引用指针)、方法返回地址等。其中long和double会占用2个本地变量空间(32bit),其余占用1个。本地变量表在进入方法时进行分配,当进入一个方法时,这个方法需要在帧中分配多大的本地变量是一件完全确定的事情,在方法运行期间不改变本地变量表的大小。

在VM Spec中对这个区域规定了2中异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

3.本地方法栈(Native Method Stacks)

本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。

4.Java堆(Java Heap)

对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。这一点在VM Spec中的描述是:所有的实例以及数组都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和标量替换优化技术出现后,VM Spec的描述就显得并不那么准确了。

Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存,在本章中我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的细节,可参见本文第二章《JVM内存管理:深入垃圾收集器与内存分配策略》。

根据VM Spec的要求,Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5.方法区(Method Area)

叫“方法区”可能认识它的人还不太多,如果叫永久代(Permanent Generation)它的粉丝也许就多了。它还有个别名叫做Non-Heap(非堆),但是VM Spec上则描述方法区为堆的一个逻辑部分(原文:the method area is logically part of the heap),这个名字的问题还真容易令人产生误解,我们在这里就不纠结了。

方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。VM Space描述中对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。

6.运行时常量池(Runtime Constant Pool)

Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。

运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。

7.本机直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。但是这部分内存也会导致OutOfMemoryError异常出现,因此我们放到这里一起描述。

在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据。

显然本机直接内存的分配不会受到Java堆大小的限制,但是即然是内存那肯定还是要受到本机物理内存(包括SWAP区或者Windows虚拟内存)的限制的,一般服务器管理员配置JVM参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),而导致动态扩展时出现OutOfMemoryError异常。

实战OutOfMemoryError

上述区域中,除了程序计数器,其他在VM Spec中都描述了产生OutOfMemoryError(下称OOM)的情形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。下文的代码都是基于Sun Hotspot虚拟机1.6版的实现,对于不同公司的不同版本的虚拟机,参数与程序运行结果可能结果会有所差别。

Java**堆**

Java堆存放的是对象实例,因此只要不断建立对象,并且保证GC Roots到对象之间有可达路径即可产生OOM异常。测试中限制Java堆大小为20M,不可扩展,通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现OOM异常的时候Dump出内存映像以便分析。(关于Dump映像文件分析方面的内容,可参见本文第三章《JVM内存管理:深入JVM内存异常分析与调优》。)

清单1:Java堆OOM测试 ///

/* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

/* @author zzm

/*/

public class HeapOOM {

   static class OOMObject {

   }



   public static void main(String[] args) {

          List<OOMObject> list = new ArrayList<OOMObject>();



          while (true) {

                 list.add(new OOMObject());

          }

   }

}

运行结果: java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid3404.hprof ...

Heap dump file created [22045981 bytes in 0.663 secs]

VM**栈和本地方法栈**

Hotspot虚拟机并不区分VM栈和本地方法栈,因此-Xoss参数实际上是无效的,栈容量只由-Xss参数设定。关于VM栈和本地方法栈在VM Spec描述了两种异常:StackOverflowError与OutOfMemoryError,当栈空间无法继续分配分配时,到底是内存太小还是栈太大其实某种意义上是对同一件事情的两种描述而已,在笔者的实验中,对于单线程应用尝试下面3种方法均无法让虚拟机产生OOM,全部尝试结果都是获得SOF异常。

1.使用-Xss参数削减栈内存容量。结果:抛出SOF异常时的堆栈深度相应缩小。

2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出SOF异常时的堆栈深度相应缩小。

3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得JIT编译器允许对象拆分后在栈中分配。结果:实际效果同第二点。

清单2:VM栈和本地方法栈OOM测试(仅作为第1点测试程序) ///

/* VM Args:-Xss128k

/* @author zzm

/*/

public class JavaVMStackSOF {

   private int stackLength = 1;



   public void stackLeak() {

          stackLength++;

          stackLeak();

   }



   public static void main(String[] args) throws Throwable {

          JavaVMStackSOF oom = new JavaVMStackSOF();

          try {

                 oom.stackLeak();

          } catch (Throwable e) {

                 System.out.println("stack length:" + oom.stackLength);

                 throw e;

          }

   }

}

运行结果: stack length:2402

Exception in thread "main" java.lang.StackOverflowError

    at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)

    at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)

    at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)

如果在多线程环境下,不断建立线程倒是可以产生OOM异常,但是基本上这个异常和VM栈空间够不够关系没有直接关系,甚至是给每个线程的VM栈分配的内存越多反而越容易产生这个OOM异常。

原因其实很好理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows限制为2G,Java堆和方法区的大小JVM有参数可以限制最大值,那剩余的内存为2G(操作系统限制)-Xmx(最大堆)-MaxPermSize(最大方法区),程序计数器消耗内存很小,可以忽略掉,那虚拟机进程本身耗费的内存不计算的话,剩下的内存就供每一个线程的VM栈和本地方法栈瓜分了,那自然每个线程中VM栈分配内存越多,就越容易把剩下的内存耗尽。

清单3:创建线程导致OOM异常 ///

/* VM Args:-Xss2M (这时候不妨设大些)

/* @author zzm

/*/

public class JavaVMStackOOM {

   private void dontStop() {

          while (true) {

          }

   }



   public void stackLeakByThread() {

          while (true) {

                 Thread thread = new Thread(new Runnable() {

                        @Override

                        public void run() {

                               dontStop();

                        }

                 });

                 thread.start();

          }

   }



   public static void main(String[] args) throws Throwable {

          JavaVMStackOOM oom = new JavaVMStackOOM();

          oom.stackLeakByThread();

   }

}

特别提示一下,如果读者要运行上面这段代码,记得要存盘当前工作,上述代码执行时有很大令操作系统卡死的风险。

运行结果: Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

运行时常量池


要在常量池里添加内容,最简单的就是使用String.intern()这个Native方法。由于常量池分配在方法区内,我们只需要通过-XX:PermSize和-XX:MaxPermSize限制方法区大小即可限制常量池容量。实现代码如下:

清单4:运行时常量池导致的OOM异常 ///

/* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M

/* @author zzm

/*/

public class RuntimeConstantPoolOOM {

   public static void main(String[] args) {

          // 使用List保持着常量池引用,压制Full GC回收常量池行为

          List<String> list = new ArrayList<String>();

          // 10M的PermSize在integer范围内足够产生OOM了

          int i = 0;

          while (true) {

                 list.add(String.valueOf(i++).intern());

          }

   }

}

运行结果: Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

   at java.lang.String.intern(Native Method)

   at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

方法区

上文讲过,方法区用于存放Class相关信息,所以这个区域的测试我们借助CGLib直接操作字节码动态生成大量的Class,值得注意的是,这里我们这个例子中模拟的场景其实经常会在实际应用中出现:当前很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区用于保证动态生成的Class可以加载入内存。

清单5:借助CGLib使得方法区出现OOM异常 ///

/* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M

/* @author zzm

/*/

public class JavaMethodAreaOOM {

   public static void main(String[] args) {

          while (true) {

                 Enhancer enhancer = new Enhancer();

                 enhancer.setSuperclass(OOMObject.class);

                 enhancer.setUseCache(false);

                 enhancer.setCallback(new MethodInterceptor() {

                        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

                               return proxy.invokeSuper(obj, args);

                        }

                 });

                 enhancer.create();

          }

   }



   static class OOMObject {



   }

}

运行结果: Caused by: java.lang.OutOfMemoryError: PermGen space

   at java.lang.ClassLoader.defineClass1(Native Method)

   at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)

   at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

   ... 8 more

本机直接内存

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,不指定的话默认与Java堆(-Xmx指定)一样,下文代码越过了DirectByteBuffer,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是基本上只有rt.jar里面的类的才能使用),因为DirectByteBuffer也会抛OOM异常,但抛出异常时实际上并没有真正向操作系统申请分配内存,而是通过计算得知无法分配既会抛出,真正申请分配的方法是unsafe.allocateMemory()。

///

/* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M

/* @author zzm

/*/

public class DirectMemoryOOM {

   private static final int _1MB = 1024 /* 1024;



   public static void main(String[] args) throws Exception {

          Field unsafeField = Unsafe.class.getDeclaredFields()[0];

          unsafeField.setAccessible(true);

          Unsafe unsafe = (Unsafe) unsafeField.get(null);

          while (true) {

                 unsafe.allocateMemory(_1MB);

          }

   }

}

运行结果: Exception in thread "main" java.lang.OutOfMemoryError

   at sun.misc.Unsafe.allocateMemory(Native Method)

   at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)

总结

到此为止,我们弄清楚虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码、操作可能导致OOM异常。虽然Java有垃圾收集机制,但OOM仍然离我们并不遥远,本章内容我们只是知道各个区域OOM异常出现的原因,下一章我们将看看Java垃圾收集机制为了避免OOM异常出现,做出了什么样的努力。 JVM内存管理:深入垃圾收集器与内存分配策 ... | 深入理解JVM

评论 共 5 条 请登录后发表评论

5 楼 巴海和 2013-06-26 21:38

前辈,能够讲一下关于类被加载到内存后,它运行过程是什么?就是讲一下方法区、栈区、程序计数器是如何相互配合完成类的运行的?谢谢

4 楼 zhifeidie12 2013-02-26 23:58

3 楼 独爱Java 2011-04-14 13:19

谢谢作者的好文章,受教了!!!

2 楼 Nathanyu 2011-04-11 12:32

有了这资料 正好把基础打好点.

1 楼 sd6733531 2011-02-22 11:20

看了前辈的讲解对JVM有了更深入的了解了

发表评论

您还没有登录,请您登录后再发表评论

New-page

文章信息

知识库: 高级语言虚拟机

相关讨论

© 2003-2012 ITeye.com. [ 京ICP证110151号 京公网安备110105010620 ] 百联优力(北京)投资有限公司 版权所有

JVM内存管理:深入垃圾收集器与内存分配策略

Posted on

JVM内存管理:深入垃圾收集器与内存分配策略 - 高级语言虚拟机

您还未登录 ! 登录 注册

ITeye3.0

群组首页编程语言高级语言虚拟机知识库JVM基础JVM内存管理:深入垃圾收集器与内存分配策略 原创作者: IcyFenix 阅读:6552次 评论:6条 更新时间:2011-05-26

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。 概述: 说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项技术当做Java语言的伴生产物。事实上GC的历史远远比Java来得久远,在1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期,人们就在思考GC需要完成的3件事情:哪些内存需要回收?什么时候回收?怎么样回收? 经过半个世纪的发展,目前的内存分配策略与垃圾回收技术已经相当成熟,一切看起来都进入“自动化”的时代,那为什么我们还要去了解GC和内存分配?答案很简单:当需要排查各种内存溢出、泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术有必要的监控、调节手段。 把时间从1960年拨回现在,回到我们熟悉的Java语言。本文第一章中介绍了Java内存运行时区域的各个部分,其中程序计数器、VM栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的帧随着方法进入、退出而有条不紊的进行着出栈入栈操作;每一个帧中分配多少内存基本上是在Class文件生成时就已知的(可能会由JIT动态晚期编译进行一些优化,但大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收具备很高的确定性,因此在这几个区域不需要过多考虑回收的问题。而Java堆和方法区(包括运行时常量池)则不一样,我们必须等到程序实际运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,我们本文后续讨论中的“内存”分配与回收仅仅指这一部分内存。 对象已死? 在堆里面存放着Java世界中几乎所有的对象,在回收前首先要确定这些对象之中哪些还在存活,哪些已经“死去”了,即不可能再被任何途径使用的对象。 引用计数算法(Reference Counting) 最初的想法,也是很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。 客观的说,引用计数算法实现简单,判定效率很高,在大部分情况下它都是一个不错的算法,但引用计数算法无法解决对象循环引用的问题。举个简单的例子:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,但是引用计数算法却无法回收他们。 根搜索算法(GC Roots Tracing) 在实际生产的语言中(Java、C/#、甚至包括前面提到的Lisp),都是使用根搜索算法判定对象是否存活。算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。在Java语言中,GC Roots包括: 1.在VM栈(帧中的本地变量)中的引用 2.方法区中的静态引用 3.JNI(即一般说的Native方法)中的引用 生存还是死亡? 判定一个对象死亡,至少经历两次标记过程:如果对象在进行根搜索后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并在稍后执行他的finalize()方法(如果它有的话)。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这点是必须的,否则一个对象在finalize()方法执行缓慢,甚至有死循环什么的将会很容易导致整个系统崩溃。finalize()方法是对象最后一次逃脱死亡命运的机会,稍后GC将进行第二次规模稍小的标记,如果在finalize()中对象成功拯救自己(只要重新建立到GC Roots的连接即可,譬如把自己赋值到某个引用上),那在第二次标记时它将被移除出“即将回收”的集合,如果对象这时候还没有逃脱,那基本上它就真的离死不远了。 需要特别说明的是,这里对finalize()方法的描述可能带点悲情的艺术加工,并不代表笔者鼓励大家去使用这个方法来拯救对象。相反,笔者建议大家尽量避免使用它,这个不是C/C++里面的析构函数,它运行代价高昂,不确定性大,无法保证各个对象的调用顺序。需要关闭外部资源之类的事情,基本上它能做的使用try-finally可以做的更好。 关于方法区 方法区即后文提到的永久代,很多人认为永久代是没有GC的,《Java虚拟机规范》中确实说过可以不要求虚拟机在这区实现GC,而且这区GC的“性价比”一般比较低:在堆中,尤其是在新生代,常规应用进行一次GC可以一般可以回收70%~95%的空间,而永久代的GC效率远小于此。虽然VM Spec不要求,但当前生产中的商业JVM都有实现永久代的GC,主要回收两部分内容:废弃常量与无用类。这两点回收思想与Java堆中的对象回收很类似,都是搜索是否存在引用,常量的相对很简单,与对象类似的判定即可。而类的回收则比较苛刻,需要满足下面3个条件: 1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。 2.加载该类的ClassLoader已经被GC。 3.该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。 是否对类进行回收可使用-XX:+ClassUnloading参数进行控制,还可以使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载、卸载信息。 在大量使用反射、动态代理、CGLib等bytecode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证永久代不会溢出。 垃圾收集算法 在这节里不打算大量讨论算法实现,只是简单的介绍一下基本思想以及发展过程。最基础的搜集算法是“标记-清除算法”(Mark-Sweep),如它的名字一样,算法分层“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象,整个过程其实前一节讲对象标记判定的时候已经基本介绍完了。说它是最基础的收集算法原因是后续的收集算法都是基于这种思路并优化其缺点得到的。它的主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。 为了解决效率问题,一种称为“复制”(Copying)的搜集算法出现,它将可用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次过清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。 现在的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM有专门研究表明新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次过拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。Sun Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有10%以内的对象存活,当survivor空间不够用时,需要依赖其他内存(譬如老年代)进行分配担保(Handle Promotion)。 复制收集算法在对象存活率高的时候,效率有所下降。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。因此人们提出另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。 当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法,这种算法并没有什么新的思想出现,只是根据对象不同的存活周期将内存划分为几块。一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。 垃圾收集器 垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。本文讨论的收集器基于Sun Hotspot虚拟机1.6版。 图1.Sun JVM1.6的垃圾收集器 图1展示了1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。在介绍着些收集器之前,我们先明确一个观点:没有最好的收集器,也没有万能的收集器,只有最合适的收集器。 1.Serial收集器 单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。 2.ParNew收集器 ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。 3.Parallel Scavenge收集器 Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。 4.Serial Old收集器 Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。 5.Parallel Old收集器 老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。 6.CMS(Concurrent Mark Sweep)收集器 CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。 内存分配与回收策略 了解GC其中很重要一点就是了解JVM的内存分配策略:即对象在哪里分配和对象什么时候回收。 关于对象在哪里分配,往大方向讲,主要就在堆上分配,但也可能经过JIT进行逃逸分析后进行标量替换拆散为原子类型在栈上分配,也可能分配在DirectMemory中(详见本文第一章)。往细节处讲,对象主要分配在新生代eden上,也可能会直接老年代中,分配的细节决定于当前使用的垃圾收集器类型与VM相关参数设置。我们可以通过下面代码来验证一下Serial收集器(ParNew收集器的规则与之完全一致)的内存分配和回收的策略。读者看完Serial收集器的分析后,不妨自己根据JVM参数文档写一些程序去实践一下其它几种收集器的分配策略。 清单1:内存分配测试代码 Java代码 收藏代码

  1. public class YoungGenGC {
  2. private static final int _1MB = 1024 /* 1024;
  3. public static void main(String[] args) {
  4. // testAllocation();
  5. testHandlePromotion();
  6. // testPretenureSizeThreshold();
  7. // testTenuringThreshold();
  8. // testTenuringThreshold2();
  9. }
  10. ///
  11. /* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
  12. /*/
  13. @SuppressWarnings("unused")
  14. public static void testAllocation() {
  15. byte[] allocation1, allocation2, allocation3, allocation4;
  16. allocation1 = new byte[2 /* _1MB];
  17. allocation2 = new byte[2 /* _1MB];
  18. allocation3 = new byte[2 /* _1MB];
  19. allocation4 = new byte[4 /* _1MB]; // 出现一次Minor GC
  20. }
  21. ///
  22. /* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
  23. /* -XX:PretenureSizeThreshold=3145728
  24. /*/
  25. @SuppressWarnings("unused")
  26. public static void testPretenureSizeThreshold() {
  27. byte[] allocation;
  28. allocation = new byte[4 /* _1MB]; //直接分配在老年代中
  29. }
  30. ///
  31. /* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
  32. /* -XX:+PrintTenuringDistribution
  33. /*/
  34. @SuppressWarnings("unused")
  35. public static void testTenuringThreshold() {
  36. byte[] allocation1, allocation2, allocation3;
  37. allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
  38. allocation2 = new byte[4 /* _1MB];
  39. allocation3 = new byte[4 /* _1MB];
  40. allocation3 = null;
  41. allocation3 = new byte[4 /* _1MB];
  42. }
  43. ///
  44. /* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
  45. /* -XX:+PrintTenuringDistribution
  46. /*/
  47. @SuppressWarnings("unused")
  48. public static void testTenuringThreshold2() {
  49. byte[] allocation1, allocation2, allocation3, allocation4;
  50. allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空间一半
  51. allocation2 = new byte[_1MB / 4];
  52. allocation3 = new byte[4 /* _1MB];
  53. allocation4 = new byte[4 /* _1MB];
  54. allocation4 = null;
  55. allocation4 = new byte[4 /* _1MB];
  56. }
  57. ///
  58. /* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
  59. /*/
  60. @SuppressWarnings("unused")
  61. public static void testHandlePromotion() {
  62. byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
  63. allocation1 = new byte[2 /* _1MB];
  64. allocation2 = new byte[2 /* _1MB];
  65. allocation3 = new byte[2 /* _1MB];
  66. allocation1 = null;
  67. allocation4 = new byte[2 /* _1MB];
  68. allocation5 = new byte[2 /* _1MB];
  69. allocation6 = new byte[2 /* _1MB];
  70. allocation4 = null;
  71. allocation5 = null;
  72. allocation6 = null;
  73. allocation7 = new byte[2 /* _1MB];
  74. }
  75. }
    public class YoungGenGC { private static final int _1MB = 1024 / 1024; public static void main(String[] args) { // testAllocation(); testHandlePromotion(); // testPretenureSizeThreshold(); // testTenuringThreshold(); // testTenuringThreshold2(); } /// / VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 // @SuppressWarnings("unused") public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 / _1MB]; allocation2 = new byte[2 / _1MB]; allocation3 = new byte[2 / _1MB]; allocation4 = new byte[4 / _1MB]; // 出现一次Minor GC } /// / VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 / -XX:PretenureSizeThreshold=3145728 // @SuppressWarnings("unused") public static void testPretenureSizeThreshold() { byte[] allocation; allocation = new byte[4 / _1MB]; //直接分配在老年代中 } /// / VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 / -XX:+PrintTenuringDistribution // @SuppressWarnings("unused") public static void testTenuringThreshold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置 allocation2 = new byte[4 / _1MB]; allocation3 = new byte[4 / _1MB]; allocation3 = null; allocation3 = new byte[4 / _1MB]; } /// / VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 / -XX:+PrintTenuringDistribution // @SuppressWarnings("unused") public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空间一半 allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 / _1MB]; allocation4 = new byte[4 / _1MB]; allocation4 = null; allocation4 = new byte[4 / _1MB]; } /// / VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure // @SuppressWarnings("unused") public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7; allocation1 = new byte[2 / _1MB]; allocation2 = new byte[2 / _1MB]; allocation3 = new byte[2 / _1MB]; allocation1 = null; allocation4 = new byte[2 / _1MB]; allocation5 = new byte[2 / _1MB]; allocation6 = new byte[2 / _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 / _1MB]; } } 规则一:通常情况下,对象在eden中分配。当eden无法分配时,触发一次Minor GC。 执行testAllocation()方法后输出了GC日志以及内存分配状况。-Xms20M -Xmx20M -Xmn10M这3个参数确定了Java堆大小为20M,不可扩展,其中10M分配给新生代,剩下的10M即为老年代。-XX:SurvivorRatio=8决定了新生代中eden与survivor的空间比例是1:8,从输出的结果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216K(eden+1个survivor)。 我们也注意到在执行testAllocation()时出现了一次Minor GC,GC的结果是新生代6651K变为148K,而总占用内存则几乎没有减少(因为几乎没有可回收的对象)。这次GC是发生的原因是为allocation4分配内存的时候,eden已经被占用了6M,剩余空间已不足分配allocation4所需的4M内存,因此发生Minor GC。GC期间虚拟机发现已有的3个2M大小的对象全部无法放入survivor空间(survivor空间只有1M大小),所以直接转移到老年代去。GC后4M的allocation4对象分配在eden中。 清单2:testAllocation()方法输出结果 [GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000) eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000) to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000) the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) No shared spaces configured. 规则二:配置了PretenureSizeThreshold的情况下,对象大于设置值将直接在老年代分配。 执行testPretenureSizeThreshold()方法后,我们看到eden空间几乎没有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation对象直接就分配在老年代中,则是因为PretenureSizeThreshold被设置为3M,因此超过3M的对象都会直接从老年代分配。 清单3: Heap def new generation total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000) eden space 8192K, 8% used [0x029d0000, 0x02a77e98, 0x031d0000) from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000) tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000) the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) compacting perm gen total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000) No shared spaces configured. 规则三:在eden经过GC后存活,并且survivor能容纳的对象,将移动到survivor空间内,如果对象在survivor中继续熬过若干次回收(默认为15次)将会被移动到老年代中。回收次数由MaxTenuringThreshold设置。 分别以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15两种设置来执行testTenuringThreshold(),方法中allocation1对象需要256K内存,survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常干净的变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代survivor空间,这时候新生代仍然有404KB被占用。 清单4: MaxTenuringThreshold=1 [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1)
  • age 1: 414664 bytes, 414664 total : 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000) eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000) tenured generation total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000) the space 10240K, 43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) No shared spaces configured. MaxTenuringThreshold=15 [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)
  • age 1: 414664 bytes, 414664 total : 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)
  • age 2: 414520 bytes, 414520 total : 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000) eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, 39% used [0x031d0000, 0x03235338, 0x032d0000) to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000) tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000) the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) No shared spaces configured. 规则四:如果在survivor空间中相同年龄所有对象大小的累计值大于survivor空间的一半,大于或等于个年龄的对象就可以直接进入老年代,无需达到MaxTenuringThreshold中要求的年龄。 执行testTenuringThreshold2()方法,并将设置-XX:MaxTenuringThreshold=15,发现运行结果中survivor占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1、allocation2对象都直接进入了老年代,而没有等待到15岁的临界年龄。因为这2个对象加起来已经到达了512K,并且它们是同年的,满足同年对象达到survivor空间的一半规则。我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了。 清单5: [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)
  • age 1: 676824 bytes, 676824 total : 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000) eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000) tenured generation total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000) the space 10240K, 46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000) No shared spaces configured. 规则五:在Minor GC触发时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,改为直接进行一次Full GC,如果小于则查看HandlePromotionFailure设置看看是否允许担保失败,如果允许,那仍然进行Minor GC,如果不允许,则也要改为进行一次Full GC。 前面提到过,新生代才有复制收集算法,但为了内存利用率,只使用其中一个survivor空间来作为轮换备份,因此当出现大量对象在GC后仍然存活的情况(最极端就是GC后所有对象都存活),就需要老年代进行分配担保,把survivor无法容纳的对象直接放入老年代。与生活中贷款担保类似,老年代要进行这样的担保,前提就是老年代本身还有容纳这些对象的剩余空间,一共有多少对象在GC之前是无法明确知道的,所以取之前每一次GC晋升到老年代对象容量的平均值与老年代的剩余空间进行比较决定是否进行Full GC来让老年代腾出更多空间。 取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,大大高于平均值的话,依然会导致担保失败,这样就只好在失败后重新进行一次Full GC。虽然担保失败时做的绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure打开,避免Full GC过于频繁。 清单6: HandlePromotionFailure = false [GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] [GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] HandlePromotionFailure = true [GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 总结 本章介绍了垃圾收集的算法、6款主要的垃圾收集器,以及通过代码实例具体介绍了新生代串行收集器对内存分配及回收的影响。 GC在很多时候都是系统并发度的决定性因素,虚拟机之所以提供多种不同的收集器,提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最好的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也没有什么必然的行为。笔者看过一些文章,撇开具体场景去谈论老年代达到92%会触发Full GC(92%应当来自CMS收集器触发的默认临界点)、98%时间在进行垃圾收集系统会抛出OOM异常(98%应该来自parallel收集器收集时间比率的默认临界点)其实意义并不太大。因此学习GC如果要到实践调优阶段,必须了解每个具体收集器的行为、优势劣势、调节参数。 JVM内存管理:深入Java内存区域与OOM

评论 共 6 条 请登录后发表评论

6 楼 newjunwei 2013-07-09 16:52

5 楼 junedo 2013-05-31 13:19

引用

ParNew收集器 介绍中 “对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器” 为什么我用hotspot 1.6.0_37 在windows 32 和linux 64环境下指定 -server时,新生代的收集器都是PS Scavenge?是不是此处描述存在错误呢?

4 楼 PV_love 2013-04-03 14:11

thetcc 写道

-XX:SurvivorRatio=8决定了新生代中eden与survivor的空间比例是1:8,从输出的结果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息 这段说反了吧?eden是较大的空间,survivor是较小的空间,比例8:1

3 楼 scgyyc820802 2011-11-25 16:50

好文章,如果我能早点看到它,半年前的一个客户就不会退货了。 程序没有万能,只有最合适。

2 楼 thetcc 2011-08-31 14:48

-XX:SurvivorRatio=8决定了新生代中eden与survivor的空间比例是1:8,从输出的结果也清晰的看到“eden space 8192K、from space 1024K、to space 1024K”的信息

1 楼 qepwqnp 2011-05-05 20:28

不错,顶一个。 大概看了下,下来再细看

发表评论

您还没有登录,请您登录后再发表评论

New-page

文章信息

知识库: 高级语言虚拟机

相关讨论

© 2003-2012 ITeye.com. [ 京ICP证110151号 京公网安备110105010620 ] 百联优力(北京)投资有限公司 版权所有

JVM知识点题目

Posted on

JVM知识点题目

BlueDavy之技术Blog

理论不懂就实践,实践不会就学理论!

JVM知识点题目

JVM是Java程序的运行环境,因此对于JVM的掌握有助于理解Java程序的执行以及编写,尤其是运行时碰到的一些诡异问题,那么怎么样能考察自己对于JVM关键知识点的掌握情况,帮助学习JVM机制呢,在这篇blog中来探讨下。 对于Java程序而言,JVM的关键机制有:字节码的加载、方法的执行、对象内存的分配和回收、线程和锁机制,这几个机制涉及到的jvm的知识点远没有写这几个字这么简单,里面的复杂度还是非常高的。 字节码的加载 JVM通过ClassLoader来完成字节码的动态加载,这里面涉及到的主要是ClassLoader的双亲委派、ClassLoader的编写方法、Class是否被加载的唯一标识以及Class的加载过程。 在考察的时候我觉得可以以这么两道简单的题来考察: 1、写一段将目录中指定的.class文件加载到JVM的程序,并通过Class对象获取到完整类名等信息; 2、一段展示代码,里面包含一个全局静态整型变量,问如果用两个ClassLoader加载此对象,执行这个整型变量++操作后结果会是怎么样的? 方法的执行 JVM有自己的一套指令系统,字节码中即已经是指令了,需要大概掌握了JVM对static、interface、instance、构造器采用的不同的执行方法,另外就是JVM中反射的实现(可以以Sun JDK来举例)、动态代理的实现,最后相关的就是JVM执行字节码的方式(解释、JIT、Hotspot),以及什么时候触发编译成机器码,如何控制。 在考察的时候我觉得可以以这么三道题来考察: 1、A a=new A();a.execute();和IA a=new A();a.execute();执行有什么不同; 2、反射的性能低的原因是? 3、编写一段程序,动态的创建一个接口的实现,并加载到JVM中执行;(可以允许用BCEL等工具) 对象内存的分配和回收 这块涉及的知识点也是比较的多,例如JVM内存区域的划分、自然类型和引用类型的内存分配的不同、TLAB、GC的算法、Sun JDK对于GC的实现、GC触发的时机、GC的跟踪和分析的方法。 在考察的时候我觉得可以以这么三道题来考察: 1、经典的String比较程序题: String a="a"; String b="b"; String ab="ab"; (a+b)==ab; ?? (引深题,如何才能让(a+b)==ab) ("a"+"b")==ab; ?? 2、写一段程序,让其OutOfMemory,或频繁执行Minor GC,但又不触发Full GC,又或频繁执行Full GC,但不执行minor GC,而且不OutOfMemory,甚至可以是控制几次Minor GC后发生一次Full GC; 3、详细讲解GC的实现,例如minor GC的时候导致是怎么回收对象内存的,Full GC的时候是怎么回收对象内存的。 线程和锁机制 这块涉及的知识点仍然是非常的多,例如线程中变量的操作机制、线程调度机制、线程的状态以及控制方法、线程的跟踪和分析方法、同步关键字、lock/unlock的原理等。 在考察的时候我觉得可以以这么几道题考察下: 1、i++的执行过程; 2、一个线程需要等待另外一个线程将某变量置为true才继续执行,如何编写这段程序,或者如何控制多个线程共同启动等; 3、控制线程状态的转换的方法,或者给几个thread dump,分析下哪个线程有问题,问题出在哪; 4、static属性加锁、全局变量属性加锁、方法加锁的不同点?

posted on 2009-03-27 14:30 BlueDavy 阅读(7897) 评论(9) 编辑 收藏 所属分类: Java

[

评论

]()

希望不是你当面试官,哈 回复 更多评论

JVM的东西的确比较复杂 回复 更多评论

都不是很懂,什么时候贴答案啊? 回复 更多评论

看了问题 很想知道答案 看书去咯 回复 更多评论

关于ClassLoader的两个问题 第一个题目把文件系统中的.class文件读到一个bute数组中,使用ClassLoader的defineClass方法把这个byte数组传递给jvm,则返回一个Class对象,通过这个Class对象可以做关于一个对象的任何事情,但defineClass方法是protected的,所以要写一个ClassLoader的子类,也就是实现一个自己的ClassLoader。 第二个题目由于Java中ClassLoader本质上是定义了一个Class的集合,而且ClassLoader之间是项目隔离的,除非它们之间是父子关系,但即使这样,只能是子ClassLoader能看到父ClassLoader定义的Class,父ClassLoader看不到子ClassLoader定义的Class,所以使用一个ClassLoader加载的类的静态变量执行++操作,对另一个ClassLoader加载的对象的静态变量是没有影响的。 班门弄斧了,还请博主指正我的回答是否正确。 回复 更多评论

老大,给个答案啊,别变成炫耀贴了。 回复 更多评论

这题太狠了: 3、编写一段程序,动态的创建一个接口的实现,并加载到JVM中执行;(可以允许用BCEL等工具) 回复 更多评论

将目录里的/*.class filter出来,getName一个个来 回复 更多评论

1.1 findclass 1.2 不同classloader加载的class是不同的class,其实包名与文件名都相同 2.3用asm 框架 writeclass 回复 更多评论 新用户注册 刷新评论列表

推荐购买云服务器(15%返利+最高千元奖金) 博客园 博问 IT新闻 Java程序员招聘 标题 请输入标题 姓名 请输入你的姓名 主页 请输入验证码 验证码 /* 内容(请不要发表任何与政治相关的内容) 请输入评论内容 Remember Me? 登录 [使用Ctrl+Enter键可以直接提交] 网站导航:

博客园 IT新闻 知识库 C++博客 程序员招聘 管理 相关文章:

feedsky 抓虾 google reader 鲜果

导航

统计

  • 随笔 - 294
  • 文章 - 2
  • 评论 - 2068
  • 引用 - 1

随笔分类

随笔档案

文章档案

Blogger's

搜索

*

最新评论

阅读排行榜

评论排行榜

JVM调优总结

Posted on

JVM调优总结 -Xms -Xmx -Xmn -Xss

  1. 堆大小设置 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。 典型设置:
  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -**Xmx3550m:设置JVM最大可用内存为3550M。 -Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 -Xss128k**:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
  • java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0 -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 -XX:MaxPermSize=16m:设置持久代大小为16m。 -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
  • 回收器选择 JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
  1. 吞吐量优先的并行收集器 如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。 典型配置
  • java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 **-XX:ParallelGCThreads=20**:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC **-XX:+UseParallelOldGC**:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 **-XX:MaxGCPauseMillis=100**:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
  • 响应时间优先的并发收集器 如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。 典型配置

  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC **-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。 -XX:+UseParNewGC**:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
  • 辅助信息 JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

  • -XX:+PrintGC 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]

[Full GC 121376K->10414K(130112K), 0.0650971 secs]

  • -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可与上面两个混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
  • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用 输出形式:Application time: 0.5291524 seconds
  • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用 输出形式:Total time for which application threads were stopped: 0.0468229 seconds
  • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息 输出形式: 34.702: [GC {Heap before gc invocations=7: def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000) eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000) from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000) to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000) tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000) the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000) compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000) the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000) ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000) rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000) 34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8: def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000) eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000) from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000) to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000) tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000) the space 69632K, 4% used[0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000) compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000) the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000) ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000) rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000) } , 0.0757599 secs]
  • -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
  • 常见配置汇总
  1. 堆设置
  • -Xms:初始堆大小
  • -Xmx:最大堆大小
  • -XX:NewSize=n:设置年轻代大小
  • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
  • -XX:MaxPermSize=n:设置持久代大小
  • 收集器设置

  • -XX:+UseSerialGC:设置串行收集器

  • -XX:+UseParallelGC:设置并行收集器
  • -XX:+UseParalledlOldGC:设置并行年老代收集器
  • -XX:+UseConcMarkSweepGC:设置并发收集器
  • 垃圾回收统计信息

  • -XX:+PrintGC

  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -Xloggc:filename
  • 并行收集器设置

  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

  • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
  • 并发收集器设置

  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

  • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

四、调优总结

  1. 年轻代大小选择
  • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
  • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  • 年老代大小选择

  • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

  • 并发垃圾收集信息

  • 持久代并发收集次数
  • 传统GC信息
  • 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间,一般会提高应用的效率
  • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
  • 较小堆引起的碎片问题 因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

  • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

  • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩