JVM内存机制与垃圾收集器总结
本文目录
1. JVM内存组成结构
2. JVM内存回收
3. 垃圾收集器与算法
4. jdk1.6中class文件结构
5. jdk1.8中永久代与元空间比较
1. JVM内存组成结构
JVM栈由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示:
2. JVM内存回收
Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:把对象分为新生代(Young)、老年代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析),即分代收集算法。
a. Young(新生代)
新生代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor取过来的对象。而且,Survivor区总有一个是空的。
eden、form、to的默认比例为8:1:1
b. Tenured(老年代):
老年代存放从新生代存活的对象。一般来说老年代存放的都是生命期较长,存活率高的对象。
c. Perm(持久代)(jdk1.8中已废弃)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
举个例子:当在程序中生成对象时,正常对象会在新生代中分配空间,如果是过大的对象也可能会直接在老年代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在老年代分配)。新生代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。
通常我们说的jvm内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的新生代和老年代都是指的jvm的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。
3. 垃圾收集器与算法
JVM首先根据分代收集算法,在进行选择以下3种算法:
算法 |
适合年代 |
特点 |
标记-清除 |
老年代 |
对象存活率高,没有额外空间担保 效率低、产生很多不连续的碎片 |
复制 |
新生代 |
将可用内存折半,只使用一半,存活率低 |
标记-整理 |
老年代 |
对象存活率高,没有额外空间担保 |
收集器总结:
收集器名称 |
适合年代 |
算法 |
单线程/多 |
客户端/服务 |
串行/并行/并发 |
特点 |
Serial |
新生代 |
复制算法 |
单 |
客户端 |
串行 |
无线程交互,单线程内效率高 |
Par new |
新生代 |
复制算法 |
多 |
服务端 |
并行 |
Serial的多线程版本,服务端首选,可以与CMS配合 |
Parallel Scavenge |
新生代 |
复制算法 |
多
|
服务端 |
并行 |
可控制吞吐量,适合在后台运算不需要太多交互的场合 |
Serial old |
老年代 |
标记-整理 |
单 |
客户端 |
串行 |
Serial的老年代 |
Parallel old |
老年代 |
标记整理 |
多 |
服务端 |
并行 |
Parallel scav的老年版,ps+ps old组合使用 |
CMS |
老年代 |
标记-清除 |
多 |
服务端 |
并发 |
获取最短停顿时间,重视服务的响应速度。 工作流程: 初始标记>并发标记>重新标记>并发清除 |
G-First |
独立区域Region |
标记-整理+复制 |
多 |
服务端 |
并行与并发 |
|
4. jdk1.6中class文件结构
根据java虚拟机规范的规定,class文件格式采用一种类似于C语言结构体的伪结构体存储数据,分为两种数据类型:无符号数和表。
无符号数:属于基本的数据类型,以u1/u2/u4/u8分别代表1个、2个、4个、8个字节,主要用来描述数字、索引引用、数量值和字符串值。
表:由多个无符号数或者其他表作为数据项构成的复合数据类型,以_info结尾,用于描述有层次关系的复合结构的数据,整个class相当于一个表。
表的构成分为:
a. 魔数与class文件的版本:魔数用来区分class文件是否能把虚拟机接受;
b. 常量池:由字面量和符号引用构成
b.1 字面量:类似于java层面的常量
b.2 符号引用:由全限定名、字段名称和描述符合方法名称和描述符组成。
全限定名:类的路径,将'.'变成‘/’代替;
描述符:其实为字段类型和方法参数以及返回值;
c. 访问标志:区分是类或接口,是否是public、static、abstract;
d. 索引:分为类索引、父类索引和接口索引集合,用来确定类的继承关系;
e. 字段表:用于描述接口或者类里声明的变量,不包含方法里的局部变量;
f. 方法表:包含访问标志、名称索引、描述符、属性表;
g. 属性表:包含code属性、exceptions属性、constantValue属性,code用来存放方法代码经过编译变成的字节码,exceptions表示throws声明的异常,constantValue为来修饰静态变量。
5. jdk1.8中永久代与元空间比较
移除永久代的工作从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 的区别,以字符串常量为例:
package com.xs.test.memory; import java.util.ArrayList; import java.util.List; 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 的运行结果:
JDK 1.7的运行结果:
JDK 1.8的运行结果:
从上述结果可以看出,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 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
本文为博主原创,转载请注明:https://www.cnblogs.com/jiangds/p/11425602.html