JVM-内存结构-方法区(二)

一、方法区

1.1 定义

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息(比如class文件)、常量、静态变量、即时编译器编译后的代码等数据。

1.2 结构

在这里插入图片描述
方法区是JVM 所有线程共享。
主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与 堆 进行区分,通常又叫 非堆。 关于 方法区内存溢出 的问题会在下文中详细探讨。

在这里插入图片描述

1.3 永久代和元空间

1.3.1 PermGen(永久代)

PermGen , 就是 PermGen space ,全称是 Permanent Generation space ,是指内存的永久保存区域。这块内存主要是被JVM存放Class和Meta信息的, Class 在被 Loader 时就会被放到 PermGen space 中。

绝大部分 Java 程序员应该都见过 java.lang.OutOfMemoryError: PermGen space 这个异常。
这里的 PermGen space 其实指的就是 方法区 。不过 方法区 和 PermGen space又有一定的区别。

  • 方法区 是 JVM 的规范,所有虚拟机 必须遵守的。常见的JVM 虚拟机 Hotspot 、 JRockit(Oracle)、J9(IBM)
  • PermGen space 则是 HotSpot 虚拟机 基于 JVM 规范对 方法区 的一个落地实现, 并且只有 HotSpot 才有 PermGen space。而如 JRockit(Oracle)、J9(IBM) 虚拟机有 方法区 ,但是就没有 PermGen space。PermGen space 是 JDK7及之前, HotSpot 虚拟机 对 方法区 的一个落地实现。在JDK8被移除。
  • Metaspace(元空间)是 JDK8及之后, HotSpot 虚拟机 对 方法区 的新的实现。

JDK6、JDK7 时,方法区 就是 PermGen(永久代)。
JDK8 时,方法区就是 Metaspace(元空间)

由于方法区 主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。
JDK1.7演示:

package com.aop8.jvm.test;
 
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
 
public class PermGenOomMock{

	List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
    public static void main(String[] args) {
        URL url = null;       
        try {
            url = new File("/tmp").toURI().toURL();
            URL[] urls = {url};
            while (true){
                ClassLoader loader = new URLClassLoader(urls);
                classLoaderList.add(loader);
                loader.loadClass("com.aop8.jvm.test.TestDemo");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
本例中使用的 JDK 版本是7,指定的 PermGen 区的大小为 8M。通过每次生成不同URLClassLoader对象来加载Test类,从而生成不同的类对象,这样就能看到我们熟悉的 java.lang.OutOfMemoryError: PermGen space 异常了。

JDK1.8演示:

/**
 * 演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class main1 extends ClassLoader {//可以用来加载类的二进制字节码

    public static void main(String[] args) {
        int j = 0;
        try {
            main1 test = new main1();
            for (int i = 0; i < 10000; i++,j++) {
                //ClassWriter 作用是生产类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                //版本号,public,类名
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回 byte[]
                byte[] code = cw.toByteArray();
                //执行类的加载
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.itcast.itheima.xpp.main1.main(main1.java:26)
4865

得出结论:
JDK6 、JDK7 存在 PermGen space;
JDK8 中, Hotspot 已经没有 PermGen space ,取而代之是一个叫做 Metaspace(元空间)
1.8以前会导致永久代内存溢出java.lang.OutOfMemoryError: PermGen space
1.8以后会导致元空间内存溢出java.lang.OutOfMemoryError: Metaspace

1.3.2 Metaspace(元空间)

Metaspace(元空间)和 PermGen(永久代)类似,都是对 JVM规范中方法区的一种落地实现。

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

Oracle 移除PermGen(永久代)从从JDK7 就开始。例如,字符串内部池,已经在JDK7 中从永久代中移除。直到JDK8 的发布将宣告 PermGen(永久代)的终结。

其实,移除 PermGen 的工作从 JDK7 就开始,永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。

但永久代仍存在于JDK7 中,并没完全移除,比如:

  • 字面量 (interned strings)转移到 Java heap;
  • 类的静态变量(class statics)转移到Java heap ;
  • 符号引用(Symbols) 转移到 Native heap ;

必须知道的是 JDK6 、JDK7 依然存在 PermGen space;

1.3.3 JDK6 、JDK7、JDK8 内存溢出的示例

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());
        }
    }
}

JDK6 的运行结果:
在这里插入图片描述
JDK7 的运行结果:
在这里插入图片描述
JDK8 的运行结果:
在这里插入图片描述
从运行结果可以得出:

1)、运行时常量池 :

  • 在 JDK6 ,抛出永久代(PermGen space)异常,说明 运行时常量池 存在于 方法区;
  • 在 JDK7、JDK8 抛出堆(Java heap space)异常,说明 运行时常量池 此时在 Java堆 中;

2)、 方法区(永久代 、元空间):

JDK8 打印ignoring option PermSize=10M; support was removed in 8.0 … 警告的原因:

  • 我们都知道,JDK8时,永久代已被移除,所以不支持 -XX:PermSize=10M -XX:MaxPermSize=10M
    永久代的参数设置。
  • JDK8 的方法区是 元空间,其参数设置是 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N 。
  • 反推证出 JDK6 、 JDK7 时,永久代 还是存在的,否则打印不支持参数设置的警告。

1.3.4 元空间与本地内存

元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。默认情况下,元空间的大小仅受 本地内存 限制,但可以通过以下参数来指定元空间的大小:

  • -XX:MetaspaceSize ,初始空间大小:达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间:默认是没有限制的。

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

  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
  • -XX:MaxMetaspaceFreeRatio ,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

1.3.5 问题

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK8 中永久代 向 元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

1)字符串存在永久代中,容易出现性能问题和内存溢出。

2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

4)Oracle 可能会将HotSpot 与 JRockit 合二为一。

1.4 常量池

1.4.1 javac+javap获取反编译文件

先看一个简单的代码,并进入其控制台内:
在这里插入图片描述
然后输入javap -v HelloWorld.class进行反编译。(若找不到文件则先用javac HelloWold.java编译出.class字节码文件)

在这里插入图片描述

类的基本信息:
在这里插入图片描述
常量池:
在这里插入图片描述

类的方法定义:

在这里插入图片描述
解读:在类的方法定义下的Code内便是System.out.println(“hello world”);的虚拟机指令。getstatic获取静态变量System.out;ldc创建一个变量;invokevirtual执行一次方法调用;return方法执行结束。

1.4.2 虚拟机指令和常量池

虚拟机指令执行过程,便是去常量池查找相应的常量进行执行的。
(注:这些指令,需要学习编译原理,才能看得懂,博主也要去补课了~~)
nimg.cn/e1e17e8f1b9f4327a404855bdc5f1cbf.png)
在这里插入图片描述

1.5 运行时常量池

常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。

运行时常量池:常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

总结

  1. 方法区是逻辑上的一个概念,而元空间(或永久代)是落地的实现,也就是说元空间(或永久代)就是方法区。
  2. 方法区在jdk1.8前的实现是永久代;jdk1.8开始则转换为元空间。
  3. jdk1.8前方法区(永久代)逻辑上属于堆的一部分,即其用的是堆内存;jdk1.8开始方法区(元空间)用的是本地内存。
  4. jdk1.6及以前运行时常量存在于方法区,jdk1.7及以后运行时常量存在于堆
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值