方法区(Method Area)
Java虚拟机有一个方法区,在所有Java虚拟机线程之间是共享的。这个方法区与常规语言的编译代码的存储区很相似,和操作系统进程中的文本段也很相似。它存储每一个类的结构,如运行时的常量池、字段和方法数据、方法和构造函数的代码、包括类中、实例中以及接口实现中用到的特殊方法(见JDK)。
方法区相当于接口,JDK7中的实现被称为永久代,JDK8中的实现被称为元空间。
方法区在逻辑上是堆的一部分,但实际上,你是可以区分开堆区与方法区的,方法区也被称为非堆(non-heap),它实际上是堆,但是jvm中设置的堆的大小中却不包含方法区。
方法区类似于堆,都属于启动时jvm创建的多线程共享方法。同样的,方法区的大小可以在物理上是不连续的也很像堆,可以设置固定大小或动态扩展。方法区的大小决定了系统可以创建多少个类,如果类的数量过多,方法区会报OOM:jdk7中会报永久代内存不足,jdk8会报元空间内存不足。
关闭jvm将销毁方法区,释放方法区内存。
方法区的进化
jdk7的方法区被称为永久代。
jdk8的方法区被称为元空间。
现在,永久代已经不是很好的概念了。使用-XX:MaxPermSize设置最大的永久代空间大小,导致了大量的OOM(Out of Memory Error Exception)。jdk8中,永久代的概念将被永久抛弃,取而代之的是元空间的概念。
永久代在本质上和元空间是很像的,它们都是方法区的一种实现,但是元空间不再通过虚拟机使用内存,代替的是使用本地内存。元空间的内部框架也有所改变,但是元空间也会出现OOM(超过本地内存大小时)。
jdk7
-XX:PermSize设置永久代初始内存。
-XX:MaxPermSize设置永久代的最大值,如果使用的内存超过这个最大值会导致OOM。
jdk8及以上
-XX:MetaspaceSize及-XX:MaxMetaspaceSize代替上面两个参数,并且上面的两个参数在Jdk8中已经移弃。
元空间的大小
现在我们编写一个很简单的程序,然后使用VisualVM来观察一下这个程序的内存分配情况。
public class MMApplication {
public MMApplication(){
System.out.println("start...");
try{
Thread.sleep(100000000);
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new MMApplication();
}
}
这个程序只是向控制台输出了一句话:“start…”,用于表明应用程序已经启动,然后保持程序处于睡眠状态即可。
我们使用如下参数运行这个java程序(为了节省打包操作,可以在idea或eclipse的运行框中使用下面的jvm参数)-XX:MetaspaceSize=100M。

运行成功后,我们使用VisualVM来观察GC部分。

可以看到,元空间的大小是1.062G(不同的平台,不同的应用程序可能会有所不同),而且无论我们将元空间的初始值设置为多少,从5M-1000M,无论什么值,使用VisualVM查看到的元空间大小都不会改变。这是为什么呢?
元空间被分割为两个部分,类指针压缩空间(compressed class space)以及非类的元空间(non-class)。类指针压缩空间只保存元空间的类部分,其它则保存在非类元空间中。前者由于技术原因,需要预留为一个连续的内存范围,默认其范围是1G(注意,仅仅是预留,还没有提交)。后者根据需求分配,基本上,这是一个内存映射链,随着新元空间的分配而增长。所以它开始时很小并且持续增长(因为会根据需要添加新的映射)。这意味着,对于非类的元空间部分,保留与提交是非常紧密的结合在一起的。
这就解释了元空间的总保留大小为1.062G(1G + 一点bit),即1G的类指针压缩空间及根据需要所提交的一点空间。当然我们也可以将类指针压缩空间关闭,添加-XX:-UseCompressedClassPointers(关闭使用压缩类指针)的参数后,我们再去查看Visual的显示:
-XX:MetaspaceSize=100M -XX:-UseCompressedClassPointers

我们看到,这里的元空间只有64M,也不是我们想要设置的100M,这又是为什么呢?
不像永久代,不指定大小,元空间将耗尽系统的所有可用内存,如果内存持续超过限制,就会报OOM异常。-XX:MetaspaceSize默认值是面向平台的,会根据不同的平台有所变动,如有些平台初始化大小为21M,如果超过这个水位线(又叫做High Water Mark——高水位线),将发生全面GC,然后卸载一些不用的类(与类加载器对应的不再活动的类),然后水位线将被重置,新高水位线的大小取决于完全GC释放的大小,如果释放不够,则不大于MaxMetaspaceSize的前提下,高水位线将适当的调高。如果有足够的空间被释放,那么高水位线将进行适当的调低。
如果初始水位线设置的过低,那么JVM可能会在程序运行其间触发多次完全GC而调整水位线,为了避免发生这种情况,这个水位标记(-XX:MetaspaceSize)可以适当的设置高一些。
设置元空间的一些jvm参数
为了更好的查看到元空间的一些变化情况,需要编写一个动态生成类的小程序。我们可以使用Javassist来完成动态创建类。Javassist是一个用于处理java字节码的函数库。代码示例如下:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
import javassist.ClassPool;
public class MethodAreaApplication {
public static void main(String[] args) throws Exception {
System.out.println("starting...");
ClassPool cp = ClassPool.getDefault();
for(int i=0;i<1_500_000;i++){
if(i%50000 == 0){
System.out.println(">> Sleeping for" + i);
Thread.sleep(2000);
}
String className = "com.dokbok.methodarea.Test" + i;
cp.makeClass(className).toClass();
}
}
}
注意:以上代码在不加任何参数的情况下,在jdk9+以上是无法运行的,会报如下错误:
throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to unnamed module @16b3fc9e
如果想正常运行,需要加入下面的JVM参数
--add-opens java.base/java.lang=ALL-UNNAMED
这里结果就不展示了,可以设置下面的这些参数,再参照VisualVM展示的结果,以明确这些参数的作用:
1、-XX:MetaspaceSize=100M
设置分配给元空间的大小,当使用容量第一次超过这个值的时候会触发完全GC。所以不像永久代,为元空间设置的实始值并不是最小值,它会根据实际需要进行动态调整(调大或调小)。其实这个值可以理解为高水平线(HWM)的初始值,当元空间的使用量第一次达到这个值的时候就会触发一次完全GC。优化JDK的时候,可以将其值设置的高一些,以避免元数据引发的早期GC。
2、-XX:MaxMetaspaceSize=100M
设置可以分配给类元数据的最大本地内存,同是也是在不触发完成GC的情况下元空间扩展的最大空间。什么意思呢?
这是给元空间的所允许的最大内存,但是当达到这个值的时候会进行一次完全的GC,如果GC后空间仍不够用就会报OOM错误。如:Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
3、-XX:InitialBootClassLoaderMetaspaceSize=2M
在互联网的很多博客中,都建议调低这个4M的默认值以减少初始内存的占用量。
在JDK15中,这个选项已经被打印了一个过期警告,在下一个版本中,使用该参数将在元空间的分配上没有任何效果,我使用的当前jdk18的版本中已经将该参数删除。
这是一个纯粹的性能优化选项。InitialBootClassLoaderMetaspaceSize的默认值是4MB,为启动类加载器提供一个比较大的内存数据块的目的,是为了使后续分配的数据块的数量最小化,通常认为分配过多的数据块的代价是很昂贵的。这潜在的假设了启动类加载器将加载很大的并且通常是静态数量的类,共实这已经是不现实的,比如有很多类库是后续加载的,类的数量本就不是静态数量的,而是一个变动的量。
在即将到来的弹性元空间的大背景下,InitialBootClassLoaderMetaspaceSize甚至变得更没有意义。现在,较大的数据块将通过它们的类加载器按需提交,类似于线程堆栈。在新的方案中,启动类加载器可以获得一个大的初始数据块,其大小是固定的并且不受用户的影响。因此这个优化选项在JDK15版本以后就不再需要了。
4、-XX:MinMetaspaceFreeRatio=40
默认值是40。如果元数据可用的提交空间占元数据提交总空间的百分比小于这个值,则高水位线将提高。实际生成中,可以考虑稍微增加默认值,可以使元空间增大的速度更快(因为值越大,触发变更的次数就超大,所以元空间增长的就更快)。
5、-XX:MaxMetaspaceFreeRatio=70
默认值是70。如果元数据可用的提交空间占元数据提交总空间的百分比大于这个值,则高水位线将降低。实现生产中,可以考虑稍微增加默认值,可以降低元空间缩小的可能性。