JVM运行时数据区篇(方法区基本概述)

Posted ProChick

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM运行时数据区篇(方法区基本概述)相关的知识,希望对你有一定的参考价值。

1.堆、栈、方法区之间的关系

从线程共享与否的角度来看

从三者交互关系的角度来看

2.什么是方法区?

  • JVM规范中表明:尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。

  • 对于 HotSpot VM 而言,方法区还有一个别名叫做Non-heap( 非堆 ),目的就是要和堆分开。所以方法区可以看作是一块独立于Java堆的一块内存空间

  • 方法区与堆空间一样,是各个线程共享的内存区域

  • 方法区在JVM启动时就会被创建, 关闭JVM就会释放这个区域的内存,并且它的实际的物理内存空间中和Java堆区域一样都可以是不连续的

  • 方法区的大小和堆空间一样,也可以选择固定大小或者可拓展大小

  • 方法区的大小决定了系统可以保存多少个类信息,如果系统定义了太多的类,就会导致方法区溢出,虚拟机同样会抛出内存溢出错误

    java.lang.OutOfMemoryError:PermGen space 或者 java.lang,OutOfMemoryError:Metaspace

    程序示例

    // 哪怕是最简单的程序也会加载非常多的类信息
    public class TestMethod {
        public static void main(String[] args) {
            System.out.println("start...");
            
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("end...");
        }
    }
    

3.方法区的演进

  • 在JDK7以及之前的版本中,习惯上把方法区称为永久代

  • 实质上,方法区和永久代并不等价,等价只针对于Java的 HotSpot VM。因为在JVM规范中,对如何实现方法区没有统一要求,也就是说可以进行不同的实现,例如:JRockit VM 或者 J9 VM 中根本不存在永久代的概念,也就谈不上等价关系。

  • 从JDK8开始,则使用元数据空间取代了永久代

  • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存,这样就很大程度上减少了OOM的问题

4.方法区内存大小设置

方法区的大小不必是固定的,JVM还可以根据应用的需要动态调整

  • JDK7以及之前

    • 通过参数 -XX:PermSize 来设置永久代初始分配空间。默认值是20.75M
    • 通过参数 -XX:MaxPermSize 来设置永久代最大可分配空间。32位机器默认是64M,64位机器默认是82M
  • JDK8以及之后

    • 通过参数 -XX:MetaspaceSize 来设置元空间初始分配空间。默认值大约是21M( Windows平台 )

    • 通过参数 -XX:MaxMetaspaceSize 来设置元空间最大可分配空间。默认值是-1,即没有限制

    • 元空间的初始大小叫做最高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类,然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值

    • 如果初始化的高水位线设置过低,则上述高水位线调整情况会发生很多次。为了避免频繁地触发Full GC 进行垃圾回收,建议将 元空间的初始值设置为一个相对较高的值

5.方法区溢出过程示例

/**
 * 首先设置元空间大小
 * jdk7中:-XX:PermSize=10m -XX:MaxPermSize=10m
 * jdk8中:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class OOMTest extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            OOMTest test = new OOMTest();
            for (int i = 0; i < 10000; i++) {
                // 创建ClassWriter对象,用于生成类的二进制字节码
                ClassWriter classWriter = new ClassWriter(0);
                // 指明版本号,修饰符,类名,包名,父类,接口
                classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, 
                                  "java/lang/Object", null);
                // 返回字节数组
                byte[] code = classWriter.toByteArray();
                // 进行类的加载
                test.defineClass("Class" + i, code, 0, code.length);
                
                j++;
            }
        } finally {
            System.out.println(j);
        }
    }
}

在JDK7中测试

在JDK8中测试

解决溢出的思路

  • 要解决 OOM 异常或 heap space 的异常,一般的手段是首先通过内存映像分析工具(比如 Eclipse Memory Analyzer) 对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory 0verflow)
  • 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置
  • 如果是内存溢出,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数设置,然后与机器物理内存对比看是否还可以调大。或者从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,进而尝试减少程序运行期的内存消耗

以上是关于JVM运行时数据区篇(方法区基本概述)的主要内容,如果未能解决你的问题,请参考以下文章

JVM运行时数据区篇(方法区进阶掌握)

JVM运行时数据区篇(本地方法栈)

JVM运行时数据区篇(虚拟机栈帧结构)

JVM运行时数据区篇(基础认知)

JVM运行时数据区篇(堆空间进阶掌握)

JVM运行时数据区篇(程序计数器)