4年前 (2020-09-28)  Java系列 |   抢沙发  531 
文章评分 1 次,平均分 5.0

一. JVM内存模型

根据JVM规范,JVM内存分为五个部分:虚拟机堆栈、堆、方法区、程序计数器和本地方法堆栈。

Java内存模型-永久代PermGen和元空间Metaspace

  1. 虚拟机堆栈:每个线程都有一个私有堆栈,该堆栈在创建线程时创建。堆栈内部是一种称为“堆栈帧”的东西。每个方法将创建一个堆栈帧。堆栈帧存储局部变量表(基本数据类型和对象引用)、操作数堆栈、方法退出和其他信息。堆栈的大小可以固定,也可以动态扩展。当堆栈调用深度大于JVM允许的范围时,将抛出stackoverflowerr错误,但此深度范围不是一个常量值。我们可以按照以下步骤测试此结果:

堆栈溢出测试源代码:

package com.javakk.test.memory;

 

public class StackErrorMock {

    private static int index = 1;

 

    public void call(){

        index++;

        call();

    }

 

    public static void main(String[] args) {

        StackErrorMock mock = new StackErrorMock();

        try {

            mock.call();

        }catch (Throwable e){

            System.out.println("Stack deep : "+index);

            e.printStackTrace();

        }

    }

}

代码段1

运行三次后,可以看到每个堆栈的深度不同,输出如下

Java内存模型-永久代PermGen和元空间Metaspace

至于红框中的值是如何产生的,您需要深入研究JVM的源代码来探索它。这里不再赘述。

除了上述错误外,虚拟机堆栈还有另一个错误,即当应用程序没有空间时,将抛出OutOfMemoryError。这里有一个小细节需要注意,catch catches Throwable而不是Exception。因为StackOverflowErrorOutOfMemoryError都是Exception的子类。

2. 局部方法栈:

这部分主要涉及到虚拟机使用的本机方法。一般来说,Java应用程序程序员不需要关心这部分内容。

3. PC寄存器:

PC寄存器,也称为程序计数器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。如果执行当前的JVM方法,则当前执行的指令的地址将保存在此寄存器中;如果执行本机方法,则PC寄存器为空。

4. 堆

堆内存是JVM所有线程的共享部分,在虚拟机启动时创建。所有对象和数组都在堆上分配。这个空间可以被GC回收。当没有空间请求时,抛出OutOfMemoryError。下面我们简单地模拟堆内存溢出情况:

package com.javakk.test.memory;
 
 
 
import java.util.ArrayList;
 
import java.util.List;
 
 
 
public class HeapOomMock {
 
    public static void main(String[] args) {
 
        List<byte[]> list = new ArrayList<byte[]>();
 
        int i = 0;
 
        boolean flag = true;
 
        while (flag){
 
            try {
 
                i++;
 
List.add(new byte[1024 * 1024]);//Add a 1M size array object each time
 
            }catch (Throwable e){
 
                e.printStackTrace();
 
                flag = false;
 
 System.out.println("count="+i);//The number of times the record was run
 
            }
 
        }
 
    }
 
}

代码段2

运行以上代码,输出如下:

Java内存模型-永久代PermGen和元空间Metaspace

注意这里我指定的堆内存大小是16M,所以这里显示count=14(这个数字不固定),至于为什么是14还是其他的数字,需要根据GC日志来判断,具体原因会在文章中向大家解释。

5. 方法区域:

方法区域也由所有线程共享。主要用于存储类信息、常量池、方法数据、方法代码等。方法区域在逻辑上是堆的一部分,但为了区别于堆,通常称为“非堆”。下面将更详细地讨论方法区域中的内存溢出问题。

二. 永久代(PermGen)

大多数Java程序员应该看到java.lang.OutOfMemoryError:PermGen space“异常。这里的“PermGen空间”指的是方法区域。然而,方法面积和“PermGen空间”本质上是不同的。前者是JVM规范,后者是JVM规范的实现,只有HotSpot有“PermGen space”,而其他类型的虚拟机,如JRockit(Oracle),J9(IBM)没有“PermGen space”。由于方法区主要存储类的相关信息,因此对于动态生成的类,更容易发生永久内存溢出。最典型的情况是,在大量jsp页面的情况下,很容易出现永久内存溢出。我们现在通过动态生成类来模拟“PermGen space”的内存溢出:

package com.javakk.test.memory;

 
public class Test {

}

代码片段3:

package com.javakk.test.memory;
 
import java.io.File;
 
import java.net.URL;
 
import java.net.URLClassLoader;
 
import java.util.ArrayList;
 
import java.util.List;
 
 
 
public class PermGenOomMock{
 
    public static void main(String[] args) {
 
        URL url = null;
 
        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
 
        try {
 
            url = new File("/tmp").toURI().toURL();
 
            URL[] urls = {url};
 
            while (true){
 
                ClassLoader loader = new URLClassLoader(urls);
 
                classLoaderList.add(loader);
 
                loader.loadClass("com.paddx.test.memory.Test");
 
            }
 
        } catch (Exception e) {
 
            e.printStackTrace();
 
        }
 
    }
 
}

运行后的结果:

Java内存模型-永久代PermGen和元空间Metaspace

本例中使用的JDK版本是1.7,指定的PermGen区域是8M,每次生成不同的URLClassLoader对象时都会加载测试类,从而产生不同的类对象,这样我们就可以看到熟悉的java.lang.OutOfMemoryError:PermGen space“异常。这里之所以使用jdk1.7,是因为在jdk1.8中,HotSpot没有“PermGen-space”间隔,而是一种叫做Metaspace的东西。让我们看看元空间和PermGen空间之间的区别。

三. 元空间(Metaspace)

事实上,永久代的删除始于JDK1.7。在JDK1.7中,永久生成中存储的一些数据已被传输到Java堆或本机堆。但是,永久性的一代仍然存在于JDK1.7中,并且没有被完全删除。例如,符号已传输到本机堆;内部字符串已传输到java堆;类的静态变量已传输到java。堆。我们可以通过一个程序来比较JDK1.6和JDK1.7、JDK1.8的区别,以字符串常量为例:

package com.javakk.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 的结果:

Java内存模型-永久代PermGen和元空间Metaspace

JDK 1.7 的结果:

Java内存模型-永久代PermGen和元空间Metaspace

JDK 1.8 的结果:

Java内存模型-永久代PermGen和元空间Metaspace

从上面的结果可以看出,在JDK1.6下,会出现“PermGen Space”的内存溢出,在JDK1.7和JDK1.8中会出现堆内存溢出,JDK1.8中的PermSize和MaxPermGen没有影响。因此,您可以大致验证jdk1.7和1.8是否将字符串常量从永久生成转移到堆,并且jdk1.8中没有永久生成。现在让我们看看元空间是什么。

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

-XX: MetaspaceSize,初始空间大小。当达到此值时,它将触发垃圾回收以进行类型卸载。同时,GC会调整该值:如果释放了大量空间,则会适当降低该值;如果释放了少量空间,则在不超过MaxMetaspaceSize时适当增加该值。

-XX: MaxMetaspaceSize,最大空间,默认为无限制。

除了上面指定的两个选项外,还有两个与GC相关的属性:

-XX: MinMetaspaceFreeRatio,GC后,元空间剩余空间的最小百分比,减少为分配的空间导致的垃圾回收

-XX: MaxMetaspaceFreeRatio,GC后,元空间剩余空间的最大百分比,减少为垃圾回收所造成的空间释放

现在让我们在jdk8下重新运行代码段4,但这次我们不再指定PermSize和MaxPermSize。相反,请指定MetaSpaceSize和MaxMetaSpaceSize的大小。输出如下:

Java内存模型-永久代PermGen和元空间Metaspace

从输出可以看出,这次不再是永久代的溢出,而是元空间的溢出。

关于metaspace内存溢出排查细节也可以参考之前的文章:https://javakk.com/160.html

四. 总结

通过以上分析,您应该对JVM的内存分区有一个大致的了解,同时也了解jdk8中永久元空间的转换。但每个人都应该有一个问题,为什么要转换?因此,最后,我想总结以下原因:

  1. 字符串存在于永久代中,容易出现性能问题和内存溢出。
  2. 类和方法信息很难确定其大小,因此永久代的大小很难指定,太小容易发生永久代溢出,太大则有可能导致老年代溢出。
  3. 永久代给GC垃圾回收带来了不必要的复杂性,回收效率低。
  4. 甲骨文可能会将HotSpot和JRockit结合在一起。
 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/490.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册