JVM 和 GC

一 堆与非堆

Java 虚拟机启动时创建一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。 在JVM中堆之外的内存称为非堆内存.。
简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的。所以方法区, JVM内部处理或优化所需的内存(如JIT编译后的代码缓存), 类结构(如运行时常数池,字段和方法数据)以及方法和构造方法的代码都在非堆内存中。堆内存分配由 -Xms -Xmx等指定,非堆内存可以使用 -XX: PermSize设置初始值。
 

二 常见配置汇总 

1 堆设置 

-Xms : 初始堆大小     这个值一般和Xmx相同,避免每次垃圾回收完成后jvm重新分配内存
-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:MaxTenuringThreshold=0 : 设置垃圾最大年龄.    如果设置为0的话, 则年轻代对象不经过Survivor区, 直接进入年老代. 如果将此值设置为一个较大值, 则年轻代对象会在Survivor区进行多次复制, 这样可以增加对象再年轻代的存活时间, 增加在年轻代即被回收的概率.
 

2 收集器设置 

-XX:+UseSerialGC:设置串行收集器   小数据量.默认情况下,JDK5.0以前都是使用串行收集器
-XX:+UseParallelGC:设置并行收集器    并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等
-XX:+UseParalledlOldGC:设置并行年老代收集器    
-XX:+UseConcMarkSweepGC:设置并发收集器. 并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间.适用于应用服务器,电信领域等
 

3 垃圾回收统计信息 

-XX:+PrintGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:filename
 

4 并行收集器设置 

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数.并行收集线程数. 
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
 

5 并发收集器设置 

-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩和整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩.可能会影响性能,但是可以消除碎片
 
 
 

三 JVM调优总结(如何规避频繁gc)

1 程序内不要调用System.gc()或Runtime.gc()
2 中间件调用了自己的GC方法, 需要设置参数禁止这些GC
3 堆内存设置不要太小
4 不要频繁实例化 & 释放对象. 尽量保存并重用, 如使用StringBuffer()和String()等
5 最好将-Xms和-Xmx设为相同值. 为了优化GC, 最好让-Xmn值约等于-Xmx的1/3
 
6 年轻代的大小选择
响应时间优先的应用: 尽可能大,直到接近系统的最低响应时间限制。此时,年轻代收集发生的频率达到最低,同时减少了到达老年代的对象
吞吐量优先的应用: 尽可能大,可能到达Gbit的程度。因为对响应时间没有要求,gc可以并行进行,适用于8cpu以上的服务器
 
7 年老代大小选择 
响应时间优先的应用: 年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
 
8 较小堆引起的碎片问题
因为年老代的并发收集器使用标记-清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果 出现"碎片",可能需要进行如下配置: 
-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩. 
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩



四 Java内存划分

在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:

JVM 和 GC

 

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

程序计数器是一个比较小的内存区域, 用于指示当前线程所执行的字节码执行到了第几行, 可以理解为是当前线程的行号指示器. 字节码解释器在工作时, 会通过改变这个计数器的值来取下一条语句指令. 每个程序计数器只用来记录一个线程的行号, 所以它是线程私有的.

如果程序执行的是一个Java方法, 则计数器记录的是正在执行的虚拟机字节码指令地址; 如果正在执行的是一个本地native方法, 则计数器的值为undefined. 由于程序计数器指示记录当前指令地址, 所以不存在内存溢出的情况. 因此, 程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

 

2) 虚拟机栈(JVM Stack)

一个线程的每个方法执行时都会创建一个栈帧(Stack Frame), 栈帧中存储的有局部变量表、操作栈、动态链接、方法出口等。当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit), 其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError;不过多数Java虚拟机都允许动态扩展虚拟机栈的大小,所以线程可以一直申请栈,直到内存不足,此时会抛出 OutOfMemoryError。每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的

 

3) 本地方法栈(Native Method Statck)

本地方法栈在作用和运行机制等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

 

4)堆区(Heap)

堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存. 根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

 

5)方法区(Method Area)

在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:年轻代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。本文中,仍将使用永久代一词。

  方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

  方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一,但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

  在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

  运行时常量池是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

 

6)直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存. 比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存受到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

 

 

五 Java内存分配机制

这里说的内存分配主要指的是在堆上的分配,Java内存分配和回收的机制概括来说就是: 分代分配,  分代回收.

对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)

 

1) 年轻代(Young Generation) 停止-复制(Stop-and-copy)

对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉,这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。 

JVM 和 GC

   HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

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

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

 

有关年轻代的JVM参数

1)-XX:NewSize和-XX:MaxNewSize

用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,比如是8(hotspot默认是8),即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到老年代

3)-XX:+PrintTenuringDistribution

这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4)-XX:InitialTenuringThreshold 和 -XX:MaxTenuringThreshold

用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

 

在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对 象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。

 

 

2) 年老代(Old Generation) 标记-整理       标记-清除

对象如果在年轻代存活了足够长的时间没被清理,则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。  

如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应该少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——"card table",所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法.

 
 
3) 永久代(方法区)
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
1 类的所有实例都已经被回收
2 加载类的ClassLoader已经被回收
类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
 
 
 

六 垃圾收集器 

在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用)
JVM 和 GC
 
 
在介绍垃圾收集器之前,需要明确一点,就是在新生代采用的停止复制算法中,“停止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。
 
(1)Serial收集器:新生代收集器,使用停止-复制算法,使用一个线程进行GC,串行,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
 
(2)ParNew收集器:新生代收集器,使用停止-复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停,缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
 
(3)Parallel Scavenge 收集器:新生代收集器,使用停止-复制算法.这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),用开关参数-XX:+UseAdaptiveSizePolicy可以进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等,这个参数在ParNew下没有。
 
(4)Serial Old收集器:老年代收集器,单线程收集器,串行,使用标记-整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停.
 
(5)Parallel Old收集器:老年代收集器,多线程并行,多线程机制与Parallel Scavenge差不多,使用标记-整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
 
(6)CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记-清除算法. 优点是并发收集(用户线程可以和GC线程同时工作),停顿小。由于并发收集器不对内存空间进行压缩整理,所以运行一段时间会产生碎片,使得运行效率降低。-XX:CMSFullGCsBeforeCompaction 这个可以设置运行多少次gc后对内存空间进行压缩整理。建议使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收。  
 
(7)G1收集器:在JDK1.7中正式发布。不同于其他的分代回收算法,G1将堆空间划分成了互相独立的区块。每块既有可能属于O区,也就可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。这种将O区划分成多块的理念源于:当并发后台线程寻找可回收的对象时,有些区块可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程,但可以用相对较少的时间优先回收包含垃圾最多区块。这就是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块
 

平时工作中大多数系统都使用CMS、即使静默升级到JDK7默认仍然采用CMS、那么G1相对于CMS的区别在:

  1. G1在压缩空间方面有优势
  2. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
  3. Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
  4. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  5. G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
  6. G1会在Young GC中使用、而CMS只能在O区使用

就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:

  1. 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
  2. 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
  3. 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象
 
总结: 
新生代:Serial收集器、ParNew收集器、Parallel Scavenge 收集器
老年代:Serial Old收集器、Parallel Old收集器、CMS(Concurrent Mark Sweep)收集器、 G1 收集器(跨新生代和老年代)
 
 
注意并发(Concurrent)和并行(Parallel)的区别:
并发是指用户线程与GC线程同时执行, 不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行);
并行收集是指多个GC线程并行工作,但此时用户线程是暂停的;
所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并发的.
 
 
 

七 jdk8 Metaspace元空间的引入

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:

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());
        }
    }
}

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

JDK 1.6 的运行结果:

 JVM 和 GC

JDK 1.7的运行结果:
JVM 和 GC
JDK 1.8的运行结果:
JVM 和 GC
 从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?

  元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

现在我们在 JDK 8下重新运行一下代码段 4,不过这次不再指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize的大小。输出结果如下:

JVM 和 GC

从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。

 

四、总结

  通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

  1、字符串存在永久代中,容易出现性能问题和内存溢出。

  2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

 

八 shallow heap 和 retained heap的区别

1 shallow size

是对象本身占据的内存的大小,不包含其引用的对象。对于常规对象(非数组)的shadow size由其成员变量的数量和类型来定,而数组的shallow size则是数组元素大小的总和。

2 retained size

Retained Size=当前对象大小 + 当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C,C就是间接引用) ,并且排除被GC Roots直接或者间接引用的对象

 

换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。 
不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被被当做Garbage。 

JVM 和 GC

GC Roots直接引用了A和B两个对象。

A对象的Retained Size=A对象的Shallow Size 
B对象的Retained Size=B对象的Shallow Size + C对象的Shallow Size

 

栗子????

JVM 和 GC

 

上图中obj1的retained size为obj1、obj2、obj4的shallow size总和,并不包括obj3的shallow size,因为obj3被Gc Roots引用了,也就是说obj3除了被obj2引用,还有其他外部引用。 

 JVM 和 GC

上图中obj1的retained size为obj1、obj2、obj3、obj4的shallow size总和,因为obj3除了被obj2引用,没有其他外部引用。

 

 

ps: 

 

对于JVM中方法区,永久代,元空间以及字符串常量池的迁移和string.intern方法    https://www.cnblogs.com/hadoop-dev/p/7169252.html