JVM运行时内存划分
1,年轻代、年老代、永久代
根据对象的生命周期的长短,把对象分成不同的种类,并分别进行内存回收,这就是分代垃圾回收。
分代垃圾回收算法的主要思路:把堆分成两个或者多个子堆,每一个子堆被视为一代。在运行的过程中,优先收集那些年幼的对象,如果一个对象经过多次收集仍然存活,那么可以把这个对象转移到高一级的堆里,减少对其扫描的次数。
目前最常用的JVMHotSpot,它采用的算法为分代回收。
HotSpot把JVM中堆空间划分为三代:年轻代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)。
年轻代
被分为三个部分,一个Eden区和两个相同的Survivor区。Eden区主要用来存储新建的对象,Survivor区也被称为from和to区,Survivor区是大小相等的两块区域,在使用复制回收算法时,作为双缓存,起到内存整理的作用,因此Survivor区始终都保持一个是空的。
老年代
主要存储生命周期较长的对象、超大的对象,无法在年轻代分配的对象。
永久代
存放代码、字符串常量池、静态变量等可以持久化的数据。SunJDK把方法区实现在了永久代中。
因为永久代基本不参与垃圾回收,所以,这里重点介绍年轻代和年老代的垃圾回收算法。
新建对象优先在Eden区分配内存,如果Eden区已满,那么在创建对象的时候,就因为无法申请到空间而出发minorGc操作,minorGc主要用来对年轻代垃圾回收;把Eden区中不能被回收的对象放入空的Survivor区,另一个Survivor区里不能被垃圾回收器回收的对象也被放入到这个Survivor区,这样就能保证有一个Survivor区是空的,如果在这个过程中发现Survivor区也是满了,那么就会把这些对象拷贝到年老代,或者Survivor区并没有满,但是有些对象已经存在了非常长的时间,这些对象也将被放到老年代中,如果当老年代也被放满了,那么就会触发fullGC。
2,什么情况下会触发fullGC,如何避免?
因为fullGC是用来清理整个堆空间,包括年轻代和永久代的,所以fullGC会造成很大的资源开销。因此,通常需要尽量避免fullGC操作。
(1)调用System.gc()方法会触发fullGC。因此,在编码的时候要尽量避免调用这个方法。
(2)老年代空间不足会触发fullGC。由于老年代主要用来存储从年轻代转入的对象、大对象和大数组,因此,为了避免触发fullGC,应尽量做到让对象在MinorGC阶段被回收、不要创建过大的对象及数组。由于在MinorGC时,只有Survivor区放不下的对象才会被放入老年代,而此时只有老年代也放不下大对象才会触发fullGC,因此,另一种避免fullGC的方法为根据实际情况增大Suvivor区、老年代空间或调低触发并发GC的比率。
(3)永久代满会触发fullGC。永久代主要存放class相关信息,当永久代满的时候,也会触发fullGC。为了避免这种情况的发生,可以增大永久代空间(例如:-XX:MaxPermSize=16m设置永久代大小为16M)。为了避免Perm区满引起的fullGC,也可以开启CMS回收永久代选项(开启选项为:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled)。CMS利用和应用程序线程并发的垃圾回收线程来进行垃圾回收操作。
需要注意的是,java8中已经移除了永久代,添加一个称为元数据区的native内存区,所以,大部分类的元数据都在本地内存中分配。
3,元空间MetaSpace
在JDK1.8以前的版本中,由于类大多是static的,很少被卸载或收集,因此这部分数据被称为永久的。同时,因为class是JVM实现的一部分,而不是由应用创建的,所以又被认为是非堆内存。在jdk1.8之前的HotSpotJVM中,存放这些永久的区域被称为永久代,永久代是一片连续的堆空间。
从JDK1.7开始,HotSpot已经逐步开始把永久代的数据向其他存储空间转移了,例如在jdk1.7中把字符串常量池从永久代转移到了JVM的堆空间中,但是永久代并没有完全被移除。从JDK1.8开始,彻底把永久代从JVM中移除了,而把类的元数据放到本地化的堆内存native heap中,这一块本地化的堆内存区域被叫做Metaspace(元空间)。
为什么要移除永久代呢?
(1)由于Permanent Generation内存经常不够用或发生内存泄露,而抛出异常:java.lang.OutOfMemoryError:PermGen。尤其是在JavaWeb开发的时候经常需要动态生成类,而永久代有时一块非常小的存储空间,动态生成过多的类会导致永久代的空间被用完而导致上述异常的出现。显然元空间有非常大的存储空间,因此从一定程度上可以避免这个问题。当然,永久代的移除并不意味着内存泄露的问题就没有了,因此,仍然需要监控内存的消耗,因为内存泄露仍然会耗尽整个本地内存。
(2)移除永久代可以促进HotSpotJVM与JRockitVM的融合,因为JRockit没有永久代。
(3)在HotSpot中,每个垃圾回收器都需要专门的代码来处理存储在PermGen中的类的元数据信息。从把类的元数据从永久代转移到Metaspace后,由于Metaspace的分配具有和javaHeap相同的地址空间,因此可以实现Metaspace和JavaHeap的无缝化管理,而且简化了fullGC的过程,以致将来可以并行对元数据信息进行垃圾收集,而没有GC暂停。
Metaspace是如何进行内存分配的?
MetaspaceVm通过借鉴内存管理的方式来管理Metaspace,把原来由多个垃圾回收器完成的工作全部转移到MetaspaceVM(由C++实现)上了。MetaspaceVM实现垃圾回收的思想非常简单:类与类加载器有着相同的生命周期,也就是说,只要类加载器还存活着,在Metaspace中存储的类的元数据就不能被释放。
MetaspaceVM通过一个块分配器来管理Metaspace内存的分配。块的大小取决于类加载器的类型。MetaspaceVM维护着一个全局的可使用的块列表。当一个类加载器需要一个块的时候,它会从这个全局块列表中取走一块,然后添加到它自己维护的块列表中。当类加载器的生命周期结束的时候,它的块将被释放,从而把申请的块归还给全局块列表。每个块又被分成多个block,每个block存储一个元数据单元。
由于类的大小不是固定的,当衣蛾类加载器需要衣蛾块的时候,有可能空闲的块太小了不足以容纳当前的类。就会出现内存碎片,目前MetaspaceVM还没有使用压缩算法或者其他的方法来解决这个碎片问题。
MetaSpace主要新增了如下几个参数:
-XX:MetaspaceSize:分配给类元数据的内存单位字节
-XX:MaxMetaspaceSize:分配给类元数据空间的最大值,一旦超过此值就会触发FullGC。
-XX:MinMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的类元数据的容量的最小比例,不够就会导致垃圾回收。
-XX:MasMetaspaceFreeRatio:表示一次GC以后,为了避免增加元数据空间的大小,空闲的元数据容量的最大比例,,不够就会导致垃圾回收。
MetaSpace的引入主要有如下几个优点:
充分利用了java语言规范中的好处,类及相关的元数据与类加载器有相同的生命周期。
每个加载器有专门的存储空间。
只进行线性分配。
不会单独回收某个类
省掉了GC扫描及压缩的时间。