`

JVM学习笔记-内存分配与回收策略

    博客分类:
  • jvm
JVM 
阅读更多

程序计数器: 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。

Java 虚拟机栈: Java 方法执行的内存模型,即每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。注: java 虚拟机栈也是线程私有的,它与线程的生命周期相同。

本地方法栈: java 虚拟机栈的作用是非常相似的,其区别是虚拟机栈执行 java 方法服务,而本地方法栈是为虚拟机使用到的 Native 方法服务。

JAVA 堆: 是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎有的对象实例都在这里分配内存。也是垃圾收集器管理的主要区域。

方法区: 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。运行时常量池是方法区的一部分。 Class 文件中除了有类的版本、字段、方法、接口、描述等信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

 

1.方法区存放了要加载的类的信息、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,在一定条件下它也会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OOM的错误信息。在Sun JDK中这块区域对应Permanet Generation,又称为持久代,默认最小值为16MB,最大值为64MB,可通过-XX:PermSize及-XX:MaxPermSize来指定最小值和最大值。栈Stack(不足StackOverflowError):-Xss=1MB

2.堆用于存储对象实例及数组值,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中对象所占用的内存由GC进行回收,在32位系统上最大为2GB,64位系统上没有限制。其大小可以通过-Xms和-Xmx来控制,-Xms为最小Heap内存,默认物理内存1/64但小于1GB;-Xmx为最大Heap内存,默认物理内存1/4但小于1GB。当空余堆小于-XX:MinHeapFreeRatio=默认40%时,会增大到-Xmx;当空余堆大于-XX:MaxHeapFreeRatio=默认70%时,会减小到-Xms。为了避免频繁调整,通常-Xms=-Xmx。

3.新生代(New Generation):大多数情况下Java程序中新建的对象都从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称为S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可通过-XX:SurvivorRatio=8设置Eden与From/To比例,即Eden=8,From=1,To=1;

4.根搜索算法:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,但一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java语言里,可作为GC Roots的对象包括下面几种:
1) 虚拟机栈(栈帧中的本地变量表)中的引用对象。
2) 方法区中的类静态属性引用的对象。
3) 方法区中的常量引用的对象。
4) 本地方法栈中JNI(即一般说的Native方法)的引用对象。

5.永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量于回收Java堆中的对象非常类似,例如当系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这个时候发生内存回收并且必要的话,这个“abc”常量就会被系统“请”出常量池。类需要同时满足3个条件才能算是“无用的类”:该类所有的实例都已经被回收、加载该类的ClassLoader已经被回收、该类对应的java.lang.Class对象没有在任何地方被引用并且无法在任何地方通过反射访问该类的方法。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。

6.在大量使用反射、动态代理、GGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景 都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

7.标记-清除(Mark-Sweep)算法:分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。有两个缺点:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,会产生内存碎片。此算法适合存活对象较多的情况。

8.复制算法(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一快的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

9.现在的商业虚拟机都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。

10.标记-整理算法(Mark-Compact):标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

11.Serial收集器:这是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

12.ParNew收集器:是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。除了Serial收集器外,目前只有它能与CMS收集器配合工作。

13.Parallel 收集器的目标则是达到一个可控制的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,高吞吐量则可以最高效率的利用CPU时间,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数及直接设置吞吐量大小的-XX:GCTimeRatio参数。收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注,当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量,这种调节方式称为GC自适应的调节策略。Parallel Scavenge通过复制算法收集新生代,Parallel Old使用“标记-整理”算法收集老年代。

14.CMS收集器是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器是基于“标记-清除”算法实现的,整个过程分为4个步骤:
1) 初始标记(CMS initial mark)
2) 并发标记(CMS concurrent mark)
3) 重新标记(CMS remark)
4) 并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到得对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

15.CMS收集器有3个缺点:
1)它对CUP资源非常敏感,在并发阶段它虽然不会导致用户线程停顿,会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。
2)它无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX: CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。注:JDK1.5、JDK1.6,CMSInitiatingOccupancyFraction的默认值被设置为92%。
3)CMS是一款基于“标记-清除”算法实现的收集器,这会导致产生内存碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费附赠一个碎片整理过程,虽然空间碎片没有了,但停顿时间不得不变长了,虚拟机设计者们还提供了另一个参数-XX:CMSFullGCsBeforeCompaction用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的。

16.新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以MinorGC非常频繁,一般回收速度也比较快。

17.老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

18.Full GC触发时机:
1)System.gc(),通过-XX:_DisableExplicitGC禁止
2)旧生代空间不足(新生代对象转入、创建大对象、大数组)Full GC后仍然不足报OutOfMemoryError: Java heap space。
3)持久代空间满(加载类、反射类、调用方法较多),Full GC后仍然不足报OutOfMemoryError: PermGen space。
4)CMS GC时出现promotion failed(Young GC时From/To放不下,旧生代也放不下)和concurret mode failure(CMS GC同时有对象要放入旧生代,但空间不足)。
5)统计Young GC时要移到旧生代的对象大小,大于旧生代剩余空间。
6)RMI会1小时执行一次Full GC,可以用-Java -Dsun.rmi.dgc.client.gcInterval=3600000来设置间隔,用-XX:_DisableExplicitGC禁止RMI调用System.gc()

19.调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果
1)新生代设置过小
一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC
2)新生代设置过大
一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加
一般说来新生代占整个堆1/3比较合适
3)Survivor设置过小
导致对象从eden直接到达旧生代,降低了在新生代的存活时间
4)Survivor设置过大
导致eden过小,增加了GC频率
另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收

20.大对象直接进入老年代:所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。大对象对虚拟机的内存分配来说就是一个坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的短命大对象,写程序的时候应当避免,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集器已获取足够的连续空间来“安置”它们。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配,避免在Eden区及两个Survivor区之间发生大量的内存拷贝。注:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数。

21.长期存活的对象进入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Surivivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15)时,就会被晋升到老年代中,该阀值可通过参数-XX:MaxTenuringThreshold来设置。

22.动态对象年龄判定:虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

23.调优实战-优化Eclipse JVM:
Eclipse 3.7、Windows 32位操作系统、JDK1.6 Update 24、物理内存4G
JVM参数设置:
-Xverify:none
-Xms512m
-Xmx512m
-Xmn128m
-XX:PermSize=96m
-XX:MaxPermSize=96m
-XX:+DisableExplicitGC
-Xnoclassgc
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=85
详解:Eclipse使用者甚多,它的编译代码我们认为是可靠的,不需要在加载的时候再进行字节码验证,因此通过参数-Xverify:none禁止掉字节码验证过程。把新生代容量提升到128MB,避免新生代频繁GC;把Java堆、永久带的最大、最小容量分别固定为512MB和96MB,避免内存频繁调整。通过-XX:+DisableExplicitGC屏蔽掉System.g c(),减少Full GC。Eclipse应该是与使用者交互非常频繁的应用程序,因此使用CMS收集器进行垃圾回收。使用-Xnoclassgc参数禁止永久带的类进行回收,此参数慎用。

24.在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句:Object ob = new Object();
这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。有了Object对象的大小,我们就可以计算其他对象的大小了。
Class NewObject {
int count;
boolean flag;
Object ob;
}
其大小为:空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因为Java在对对象内存分配时都是以8的整数倍来分,因此大于17byte的最接近8的整数倍的是24,因此此对象的大小为24byte。
这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要把他们作为对象来看待。包装类型的大小至少是12byte(声明一个空Object至少需要的空间),而且12byte没有包含任何有效信息,同时,因为Java对象大小是8的整数倍,因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的,它是使用基本类型的N倍(N>2),有些类型的内存占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在JDK5.0以后,因为加入了自动类型装换,因此,Java虚拟机会在存储方面进行相应的优化。

25.在JDK 6的HotSpot VM中,Oracle/Sun有官方支持的GC只有CMS比较特殊(Garbage-First在JDK6里还没正式支持,不算在内):其它几种GC的每个周期都是完全stop-the-world的;而CMS的每个并发GC周期则有两个stop-the-world阶段——initial mark与final re-mark,其它阶段是与应用程序一起并发执行的。如果CMS并发GC过程中出现了concurrent mode failure的话那么接下来就会做一次mark-sweep-compact的full GC,这个是完全stop-the-world的。正是这个特征,使得CMS的每个并发GC周期总共会更新full GC计数器两次,initial mark与final re-mark各一次;如果出现concurrent mode failure,则接下来的full GC自己算一次。
如果说大家关心“GC次数”主要关心的其实是应用暂停次数的话,这么做倒也合理。但要注意的是在CMS里“暂停次数”并不等同于“GC次数”,CMS并发GC的一个周期叫“一次GC”但暂停了两次。
只不过有些人在从其它GC改为用CMS的时候会对“full GC次数”的显著增加感到不满,觉得是不是应该想办法调优来让“full GC次数”降下来。这里有几点:
1)CMS GC的设计初衷就是以降低GC latency为目标。如果一个应用产生垃圾的速度非常高的话,原本清除那些垃圾需要的时间并不会消失,CMS只是把它从一个大暂停分散到了多个阶段上,其中部分是暂停的,部分是并发的。所以暂停的次数本来就应该会增加,而每次停顿的时间则应该比较短——这是设计取舍的倾向性导致的。
2)为了更有效的实现并发,CMS GC进行的过程中必须保证堆里还有足够剩余空间来留给应用去分配对象,所以比起ParallelScavenge等别的实现CMS必须要提早一些触发并发GC的启动。如果从ParallelScavange迁移到CMS的时候不同时增大GC堆的大小,那么可以看到同样的应用在GC堆的占用率更低的时候就会触发GC了,所以GC次数增加了。
3)CMS GC中,“full GC次数”的计数器在每个并发GC周期里是增加2而不是增加1的。这也就是这篇日志最想说明的点:这个计数器说明了GC造成的应用暂停的次数,但并不代表CMS的并发GC周期的个数。由于full GC的计数器也会在完全stop-the-world的full GC中增加1,所以这个计数器也不准确代表并发GC周期个数的正好两倍。
4)一个CMS并发GC周期的触发原因只有一个;其中的两次暂停都是同一个原因引致的,例如说最初CMS old gen或者perm gen的使用率已经超过了某个阈值之类

26 汇总一下JVM常见配置
堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
–Xmn: 设置新生代大小
-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数。并行收集线程数。
[/size]

 

分享到:
评论
1 楼 安静听歌 2016-06-15  
   

相关推荐

Global site tag (gtag.js) - Google Analytics