堆 & 非堆内存 & GC策略

年轻代(Young) + 年老代(Tenured) + 持久代(Perm)  元空间(Metaspace)

java虚拟机规范约定堆可以处于物理不连续的内存空间中,只要逻辑上是连续的即可,类似于磁盘空间。

堆内存 = 年轻代(Young) + 年老代(Tenured)

基于对象生命周期分析,使用不同的算法进行GC。

年轻代(Young Generation)  

年轻代中的对象基本都是朝生夕死(80%以上),所以年轻代的垃圾回收算法使用的是复制算法,基本思想: 将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片 

一般而言: 年轻代 = Eden区 + 两个Survivor区(From和To) 默认比例为Eden:S0:S1==8:1:1。

所有新生成的对象首先都是放在年轻代的,年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,大部分对象在Eden区中生成。Survivor区是可以配置为多于两个的,这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。当Eden区满时进行Minor GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。

这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,Survivor区总有一个是空的,“From”与“To”是相对的。所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象。

年老代 (Tenured)

标记(Mark)算法进行回收

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

非堆

持久代(Perm)

用于存放静态文件,如Java静态类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。通过-XX:MaxPermSize=n 进行设置。

  • JVM中类的元数据在Java堆中的存储区域。
  • Java类对应的HotSpot虚拟机中的内部表示也存储在这里。
  • 类的层级信息、字段、名字、修饰符、直接接口的一个有序列表。
  • 方法的编译信息及字节码。
  • 静态(static)变量 
  • 常量池和符号解析

Metaspace

在JDK8中, classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,理论上讲如果没有触碰进程可用内存上限就meiyo

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,eg:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。

永久代在JDK8中被完全的移除了,所以永久代的参数-XX:PermSize和-XX:MaxPermSize也被移除了。

public class StringOomMock {
    static String base = "string";

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String str = base + base;
            base = str;
            list.add(str.intern()); // String类的 intern() 方法还可在运行期间把字符串放到字符串常量池中
        }
    }
}

这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存。通过 JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:

JDK 1.6 的运行结果:

JDK 1.7的运行结果:

JDK 1.8的运行结果:

5849b6caeff9e20e965da970badcf491462.jpg
结论:大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中。

为什么

"java.lang.OutOfMemoryError: PermGen space "这个异常中的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,仅仅因为HotSpot虚拟机的设计团队选择将GC分代收集扩展至方法区,这样垃圾收集就能像管理java堆一样管理永久代内存,省去专门为方法区编写内存管理代码工作。而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。

        由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

永久代的缺陷:

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 对于该区域的回收效果难以令人满意,尤其是对于类型的卸载。回收未完全会导致内存泄露

移除后:

GC的性能得到了提升:

  • Full GC中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了。
  • 元空间只有少量的指针指向Java堆。这包括:类的元数据中指向java.lang.Class实例的指针; 数组类的元数据中,指向java.lang.Class集合的指针。
  • 没有元数据压缩的开销
  • 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
  • 减少了Full GC的时间
  • G1回收器中,并发标记阶段完成后可以进行类的卸载

官方说明

参照JEP122:http://openjdk.java.net/jeps/122,原文截取:
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。 

新指令

  • -XX:MetaspaceSize: class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
  • -XX:MaxMetaspaceSize: 可以为class metadata分配的最大空间。默认是没有限制的。
  • -XX:MinMetaspaceFreeRatio: 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio: 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集

默认情况下,class metadata的分配仅受限于可用的native memory总量。可以使用MaxMetaspaceSize来限制可为class metadata分配的最大内存。当class metadata的使用的内存达到MetaspaceSize(32位clientVM默认12Mbytes,32位ServerVM默认是16Mbytes)时就会对死亡的类加载器和类进行垃圾收集。设置MetaspaceSize为一个较高的值可以推迟垃圾收集的发生。

GC

     以栈或寄存器中的引用为起点,可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。因此,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器…)。而最简单的Java栈就是Java程序执行的main函数。这即是“标记-清除”的回收方式。

Minor GC

     一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC。对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区;然后整理Survivor的两个区。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

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

Major GC 

清理老年代。但是由于很多MojorGC 是由MinorGC 触发的,所以有时候很难将MajorGC 和MinorGC区分开。

Full GC

     对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以很慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

  1.  年老代(Tenured)被写满
  2. 持久代(Perm)被写满
  3.  System.gc()被显示调用
  4. 上一次GC之后Heap的各域分配策略动态变化

GC算法

基本回收策略

  1. 引用计数(Reference Counting): 原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
  2. 标记-清除(Mark-Sweep): 第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时会产生内存碎片。
  3. 复制(Copying):  把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间
  4. 标记-整理(Mark-Compact):结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

80694156ce35e73d535fdea6e2e11df1fb2.jpg

系统线程分

串行收集:使用单线程处理所有垃圾回收工作; 

并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。

并发收集: 可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间 。相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

指令

  •  -XX:+UseSerialGC: 年轻代的串行收集。
  • -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效
  • -XX:+UseParallelOldGC: 年老代并行收集 ,默认是使用单线程进行垃圾回收。
  • -XX:ParallelGCThreads=<N>: 设置并行垃圾回收的线程数。可设置与机器处理器数量相等
  • -XX:MaxGCPauseMillis=<N>: 指定垃圾回收时的最长暂停时间。
  • -XX:GCTimeRatio=<N>: 吞吐量为垃圾回收时间与非垃圾回收时间的比值。公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。
  • -XX:+UseConcMarkSweepGC: 打开并发收集。
  • -XX:CMSInitiatingOccupancyFraction=<N>: 指定还有多少剩余堆时开始执行并发收集。  并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器 会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
  • -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。     
  • -XX:+UseConcMarkSweepGC:设置年老代为并发收集。
  • -XX:+UseParNewGC: 设置年轻代为并行收集
  • -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
  • -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能影响性能,但可消除碎片 
  • -XX:+PrintTenuringDistribution: 用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小
  • -XX:NewSize和-XX:MaxNewSize: 用于设置年轻代的大小
  • -XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold: 用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

eg.

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-Xmx3550m:设置JVM最大可用内存为3550M。

-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g:设置年轻代大小为2G。整个堆大小= 年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

-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区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

 

转载于:https://my.oschina.net/u/3434392/blog/1504212

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值