在过去(当自定义类加载器使用不普遍的时候),类几乎是“静态的”并且很少被卸载和回收,因此类也可以被看成“永久的”。另外由于类作为JVM实现的一部分,它们不由程序来创建,因为它们也被认为是“非堆”的内存。
在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫做永久代的区域。永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小,32位机器默认的永久代的大小为64M,64位的机器则为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。但是有一个明显的问题,由于我们可以通过‑XX:MaxPermSize 设置永久代的大小,一旦类的元数据超过了设定的大小,程序就会耗尽内存,并出现内存溢出错误(OOM)。
备注:在JDK7之前的HotSpot虚拟机中,纳入字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。
这边转自 http://www.cnblogs.com/moonandstar08/p/5001914.html
这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。
同时,HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据。将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化。
持久代的空间被彻底地删除了,它被一个叫元空间的区域所替代了。持久代删除了之后,很明显,JVM会忽略PermSize和MaxPermSize这两个参数,还有就是你再也看不到java.lang.OutOfMemoryError: PermGen error的异常了。
JDK 8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间。
元空间的特点:
充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
每个加载器有专门的存储空间
只进行线性分配
不会单独回收某个类
省掉了GC扫描及压缩的时间
元空间里的对象的位置是固定的
如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
元空间的内存分配模型
绝大多数的类元数据的空间都从本地内存中分配
用来描述类元数据的类也被删除了
分元数据分配了多个虚拟内存空间
给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
归还内存块,释放内存块列表
一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
减少碎片的策略
我们来看下JVM是如何给元数据分配虚拟内存的空间的
你可以看到虚拟内存空间是如何分配的(vs1,vs2,vs3) ,以及类加载器的内存块是如何分配的。CL是Class Loader的缩写。
理解_mark和_klass指针
要想理解下面这张图,你得搞清楚这些指针都是什么东西。
JVM中,每个对象都有一个指向它自身类的指针,不过这个指针只是指向具体的实现类,而不是接口或者抽象类。
对于32位的JVM:
_mark : 4字节常量
_klass: 指向类的4字节指针 对象的内存布局中的第二个字段( _klass,在32位JVM中,相对对象在内存中的位置的偏移量是4,64位的是8)指向的是内存中对象的类定义。
64位的JVM:
_mark : 8字节常量
_klass: 指向类的8字节的指针
开启了指针压缩的64位JVM: _mark : 8字节常量
_klass: 指向类的4字节的指针
Java对象的内存布局
类指针压缩空间(Compressed Class Pointer Space)
只有是64位平台上启用了类指针压缩才会存在这个区域。对于64位平台,为了压缩JVM对象中的_klass指针的大小,引入了类指针压缩空间(Compressed Class Pointer Space)。
压缩指针后的内存布局
指针压缩概要
64位平台上默认打开
使用-XX:+UseCompressedOops压缩对象指针 "oops"指的是普通对象指针("ordinary" object pointers)。 Java堆中对象指针会被压缩成32位。 使用堆基地址(如果堆在低26G内存中的话,基地址为0)
使用-XX:+UseCompressedClassPointers选项来压缩类指针
对象中指向类元数据的指针会被压缩成32位
类指针压缩空间会有一个基地址
元空间和类指针压缩空间的区别
类指针压缩空间只包含类的元数据,比如InstanceKlass, ArrayKlass 仅当打开了UseCompressedClassPointers选项才生效 为了提高性能,Java中的虚方法表也存放到这里 这里到底存放哪些元数据的类型,目前仍在减少
元空间包含类的其它比较大的元数据,比如方法,字节码,常量池等。
元空间的调优
使用-XX:MaxMetaspaceSize参数可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。-XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小。
MaxMetaspaceSize的调优
-XX:MaxMetaspaceSize={unlimited}
元空间的大小受限于你机器的内存
限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。如果怀疑有类加载器出现泄露,应当使用这个参数;32位机器上,如果地址空间可能会被耗尽,也应当设置这个参数。
元空间的初始大小是21M——这是GC的初始的高水位线,超过这个大小会进行Full GC来进行类的回收。
如果启动后GC过于频繁,请将该值设置得大一些
可以设置成和持久代一样的大小,以便推迟GC的执行时间
CompressedClassSpaceSize的调优
只有当-XX:+UseCompressedClassPointers开启了才有效
-XX:CompressedClassSpaceSize=1G
由于这个大小在启动的时候就固定了的,因此最好设置得大点。
没有使用到的话不要进行设置
JVM后续可能会让这个区可以动态的增长。不需要是连续的区域,只要从基地址可达就行;可能会将更多的类元信息放回到元空间中;未来会基于PredictedLoadedClassCount的值来自动的设置该空间的大小
元空间的一些工具
jmap -permstat改成了jmap -clstats。它用来打印Java堆的类加载器的统计数据。对每一个类加载器,会输出它的名字,是否存活,地址,父类加载器,以及它已经加载的类的数量及大小。除此之外,驻留的字符串(intern)的数量及大小也会打印出来。
jstat -gc,这个命令输出的是元空间的信息而非持久代的
jcmd GC.class_stats提供类元数据大小的详细信息。使用这个功能启动程序时需要加上-XX:+UnlockDiagnosticVMOptions选项。
提高GC的性能
如果你理解了元空间的概念,很容易发现GC的性能得到了提升。
Full GC中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了。
元空间只有少量的指针指向Java堆。这包括:类的元数据中指向java/lang/Class实例的指针;数组类的元数据中,指向java/lang/Class集合的指针。
没有元数据压缩的开销
减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
减少了Full GC的时间
G1回收器中,并发标记阶段完成后可以进行类的卸载
java8中metaspace总结如下:
PermGen 空间的状况
这部分内存空间将全部移除。
JVM的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。
Metaspace 内存分配模型
大部分类元数据都在本地内存中分配。
用于描述类元数据的“klasses”已经被移除。
Metaspace 容量
默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。
新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。
Metaspace 垃圾回收
对于僵死的类及类加载器的垃圾回收将在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。
适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。
Java 堆内存的影响
一些杂项数据已经移到Java堆空间中。升级到JDK8之后,会发现Java堆 空间有所增长。
Metaspace 监控
元空间的使用情况可以从HotSpot1.8的详细GC日志输出中得到。
Jstat 和 JVisualVM两个工具,在使用b75版本进行测试时,已经更新了,但是还是能看到老的PermGen空间的出现。
前面已经从理论上充分说明,下面让我们通过“泄漏”程序进行新内存空间的观察……
PermGen vs. Metaspace 运行时比较
为了更好地理解Metaspace内存空间的运行时行为,
将进行以下几种场景的测试:
使用JDK1.7运行Java程序,监控并耗尽默认设定的85MB大小的PermGen内存空间。
使用JDK1.8运行Java程序,监控新Metaspace内存空间的动态增长和垃圾回收过程。
使用JDK1.8运行Java程序,模拟耗尽通过“MaxMetaspaceSize”参数设定的128MB大小的Metaspace内存空间。
首先建立了一个模拟PermGen OOM的代码
publicclassClassA {
publicvoidmethod(String name) {
// do nothing
}
}
上面是一个简单的ClassA,把他编译成class字节码放到D:/classes下面,测试代码中用URLClassLoader来加载此类型上面类编译成class
/**
* 模拟PermGen OOM
* @author benhail
*/
publicclassOOMTest {
publicstaticvoidmain(String[] args) {
try{
//准备url
URL url =newFile("D:/classes").toURI().toURL();
URL[] urls = {url};
//获取有关类型加载的JMX接口
ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
//用于缓存类加载器
List classLoaders =newArrayList();
while(true) {
//加载类型并缓存类加载器实例
ClassLoader classLoader =newURLClassLoader(urls);
classLoaders.add(classLoader);
classLoader.loadClass("ClassA");
//显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
System.out.println("total: "+ loadingBean.getTotalLoadedClassCount());
System.out.println("active: "+ loadingBean.getLoadedClassCount());
System.out.println("unloaded: "+ loadingBean.getUnloadedClassCount());
}
}catch(Exception e) {
e.printStackTrace();
}
}
}
虚拟机器参数设置如下:-verbose -verbose:gc
设置-verbose参数是为了获取类型加载和卸载的信息
设置-verbose:gc是为了获取垃圾收集的相关信息
JDK 1.7 @64-bit – PermGen 耗尽测试
Java1.7的PermGen默认空间为85 MB(或者可以通过-XX:MaxPermSize=XXXm指定)
可以从上面的JVisualVM的截图看出:当加载超过6万个类之后,PermGen被耗尽。我们也能通过程序和GC的输出观察耗尽的过程。
程序输出(摘取了部分)
......
[Loaded ClassA from file:/D:/classes/]
total:64887
active:64887
unloaded:0
[GC 245041K->213978K(536768K),0.0597188secs]
[Full GC 213978K->211425K(644992K),0.6456638secs]
[GC 211425K->211425K(656448K),0.0086696secs]
[Full GC 211425K->211411K(731008K),0.6924754secs]
[GC 211411K->211411K(726528K),0.0088992secs]
...............
java.lang.OutOfMemoryError: PermGen space
JDK 1.8 @64-bit – Metaspace大小动态调整测试
Java的Metaspace空间:不受限制 (默认)
从上面的截图可以看到,JVM Metaspace进行了动态扩展,本地内存的使用由20MB增长到646MB,以满足程序中不断增长的类数据内存占用需求。我们也能观察到JVM的垃圾回收事件—试图销毁僵死的类或类加载器对象。但是,由于我们程序的泄漏,JVM别无选择只能动态扩展Metaspace内存空间。程序加载超过10万个类,而没有出现OOM事件。
JDK 1.8 @64-bit – Metaspace 受限测试
Java的Metaspace空间:128MB(-XX:MaxMetaspaceSize=128m)
可以从上面的JVisualVM的截图看出:当加载超过2万个类之后,Metaspace被耗尽;与JDK1.7运行时非常相似。我们也能通过程序和GC的输出观察耗尽的过程。另一个有趣的现象是,保留的原生内存占用量是设定的最大大小两倍之多。这可能表明,如果可能的话,可微调元空间容量大小策略,来避免本地内存的浪费。
从Java程序的输出中看到如下异常。
[Loaded ClassA from file:/D:/classes/]
total:21393
active:21393
unloaded:0
[GC (Metadata GC Threshold) 64306K->57010K(111616K),0.0145502secs]
[Full GC (Metadata GC Threshold) 57010K->56810K(122368K),0.1068084secs]
java.lang.OutOfMemoryError: Metaspace
在设置了MaxMetaspaceSize的情况下,该空间的内存仍然会耗尽,进而引发“java.lang.OutOfMemoryError: Metadata space”错误。因为类加载器的泄漏仍然存在,而通常Java又不希望无限制地消耗本机内存,因此设置一个类似于MaxPermSize的限制看起来也是合理的。
总结
之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;
提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间;
隐患就是如果程序存在内存泄露,像OOMTest那样,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。
总结
Hotspot中的元数据现在存储到了元空间里。mmap中的内存块的生命周期与类加载器的一致。
类指针压缩空间(Compressed class pointer space)目前仍然是固定大小的,但它的空间较大
可以进行参数的调优,不过这不是必需的。
未来可能会增加其它的优化及新特性。比如, 应用程序类数据共享;新生代GC优化,G1回收器进行类的回收;减少元数据的大小,以及JVM内部对象的内存占用量。