JVM运行时数据区之方法区

堆空间怎么没讲方法区?

之前学堆的时候 一直疑惑 怎么没讲方法区 方法区不是在堆中的吗

现在疑惑解开了 逻辑上来说 方法区是属于堆空间的 但是实现上可以看成是独立于堆空间存在的 之前设置的新老年代比例 也没有包含方法区

还有一点概念 jdk7是永久代 jdk8以后统一被称之为元空间了

他们都是方法区的具体实现

这么来讲吧 方法区相当于接口 永久代和元空间都是接口实现类 这样就好理解了

堆 栈 方法区的关系

堆主要存放的还是对象 当然也有oom和gc 线程共享

栈存放的是引用 线程独有

方法区主要存放类型信息 线程共享 也有oom和gc

方法区的演进

之前也说过 7之前的方法区被叫做永久代 它最主要的问题在于 是放在jvm内存上的 也就是说当创建的类很多 会导致大量占用jvm空间造成oom:permgen

而8以后的方法区被叫做元空间 而是直接使用本地内存 这样能够使用的内存就更多了

如何设置方法区大小?

为了知识的完整性 还是将78两个的设置都讲一下吧

jdk7 设置永久代
设置初始大小: -XX:PermSize=100m 默认大小是20.75m
设置最大空间: -XX:MaxpermSize=100m 32位机器默认是64m 64位默认是82m

jdk8 设置元空间
这里以windows平台为例
设置初始大小: -XX:MetaspaceSize=100m 默认大小是21m
设置最大空间: -XX:MaxMetaspaceSize=100m 默认大小不受限制为 -1

这里主要的建议是 只设置元空间的初始值即可 不需要设置最大值 且初始值尽量大一点 若是设置小了 将导致频繁的gc扩容

模拟元空间OOM

用代码来演示 如何使得元空间OOM 其实就是创建很多类 再调小元空间的内存即可

代码如下 不一一解释了 主要就是创建10000个类

public class Test8 extends ClassLoader{
    public static void main(String[] args) throws InterruptedException {
        int j=0;
        Test8 test8 = new Test8();
        for (int i = 0; i < 10000; i++) {
            ClassWriter classWriter = new ClassWriter(0);
            classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null,"java/lang/Object", null);
            byte[] bytes = classWriter.toByteArray();
            test8.defineClass("Class" + i, bytes, 0, bytes.length);
            j++;
        }
        System.out.println(j);
    }
}

jvm设置参数 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:-UseCompressedClassPointers

注意最后一个参数 禁用压缩类指针 默认它是开启的 好像会对类空间有个压缩的效果 导致看不到我们想要的异常 所以选择关闭

运行程序结果如下

image.png

和我们想的一样 当类信息过多将导致方法区OOM

OOM主要分为内存泄漏和内存溢出

前者指 一个对象还有引用 但是已经很久没有被使用了 也没有被垃圾回收了 相当与一个被遗忘的对象

后者指 对应的堆或者方法区里的对象或者是类信息过多 内存放不下了 我们刚刚演示的 自然就是内存溢出了 可以通过调整内存大小来解决 当然具体情况需要具体分析

方法区的内部结构

方法区主要存储 类型信息 常量 静态变量 编译后的代码缓存等数据。

运行时常量池

要了解运行时常量池 首先就要知道常量池是什么 其实打开反编译后的字节码文件 里面就会有对应的常量池
常量池主要存放编译期生成的各种字面量与符号引用

具体命令

javap -v -p xx.class

如图所示

image.png

而经过类加载器加载到方法区后 就变成了运行时常量池

运行时常量池还具有动态性 这点我不是很能理解

方法区的演进

所有东西都会有一个交替迭代的过程 方法区也不例外

很多之前觉得很好的设计 到了未来将会被另一种理念所替代 所以方法区这些年也发生了变化

在jdk6 方法区被称为永久代 字符串常量池 静态变量也存放在永久代里

在jdk7 逐渐去除永久代 字符串常量池 静态变量被移除放入堆中

image.png

在jdk8 永久代被元空间替代 但字符串常量池和静态变量依旧放在堆中image.png

为什么要用元空间替代永久代呢?

官方原文的解释是 两个虚拟机 JRocket与Hotspot进行融合了 前者使用了元空间 后者也使用

这个解释不能让我满意 相当于你说 为什么要排队 因为前面在排队 所以我也排队

好像没问题? 但是还是没有解释根本的原因

原因其实有以下两点

1.永久代的空间大小很难确定 调小了吧 容易造成oom 调大了呢 占用整个虚拟机的内存 所以干脆给它独立出来 直接使用本地内存更加合适

2.对永久代的调优是困难的 这里稍微提一下 方法区的垃圾回收主要针对的是那些不用的常量和类 常量的垃圾回收容易判断 但是类的回收就比较苛刻了 一般来讲只有full gc的时候才会对永久代进行回收

为什么字符串常量池要被放入堆中?

这里首先声明一点 这里所说的都是变量的存放位置 而不是对象 因为对象一直是被存放在堆空间中的 new 后面的东西 被称为对象 =号左边的 被称之为变量或者引用 这点不要搞混了

将字符串常量池放入堆空间 好处在哪? 堆的特性就是频繁的垃圾回收区域 这样对比之前只有full gc才能够被回收的大量字符串常量 放在堆中 更加容易被回收

变量放在哪里?

变量主要有四个 实例变量 常量 局部变量 静态变量

实例变量是跟随对象实体被放入堆中的

常量池里包含了字符串常量 也是放在堆中的

局部变量是存放在方法栈帧中的局部变量表里的

静态变量也是放在堆里的 具体这里不再证明了

总结下 三个变量 除了局部变量 其他都是放在堆里的

方法区的垃圾回收

前面已经说过了 方法区也是有垃圾回收的 但是这在《java虚拟机规范》中 并没有强制说一定要进行垃圾回收 由不同的虚拟机自己决定 如在jdk11的zgc回收器就没有类的卸载

方法区的回收主要是回收: 废弃的常量和不再使用的类型

常量好回收 但类型的回收就要满足挺多条件的 所以很难办 但难办并不是说办不了

主要有:

  1. 某个类型对应的实例被回收了
  2. 类型对应的类加载器被回收了(这个很难做到 大多需要自定义类加载替换)
  3. 没有在反射的地方用到该类

做到了上面的三个条件 只是允许被回收 还需要jvm对应的参数设置才能够真正被回收 所以大致了解下 具体不深入了

方法区就暂时到这里了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值