icode icode
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

iqqcode

保持对技术的探索实践与热爱
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaSE

    • 面向对象

    • 常用API

    • 集合类

    • 多线程

    • JDK8新特性

    • IO流

    • JVM

      • 类加载系统
      • 运行时数据区及线程
      • 程序计数器
      • 虚拟机栈
      • 本地方法栈
      • 堆
      • 方法区
        • 1. 方法区
          • 方法区理解
          • 方法区的演进
          • 永久代为什么要被替换为元空间
          • 方法区的设置与OOM
        • 2. 方法区的内部结构
          • 类型信息
          • 域信息
          • 方法信息
        • 3. 运行时常量池
          • 运行时常量池vs常量池
        • 4. 代码追踪方法区的使用
        • 5. 方法区的垃圾回收
          • 频繁的使用反射
      • 对象实例化的内存布局与访问定位
      • 面试考点
      • 垃圾回收算法
      • 执行引擎
      • 垃圾回收相关概念
      • 垃圾回收算法
      • 垃圾回收器
      • GC日志分析
      • JVM内存区域与内存溢出异常
      • JVM垃圾回收器与内存分配策略
      • Java内存模型JMM详解
      • StringTable详解
      • 站在虚拟机栈的角度,用字节码来理解i++和++i
  • JavaWeb

  • JavaEE

  • JavaTopic

  • 设计模式

  • 计算机基础

  • Java后端
  • JavaSE
  • JVM
iqqcode
2021-06-17
目录

方法区

申明:本文是基于HotSpot来学习说明的。方法区是堆上的一个概念,具体的落地实现是永久代或者元空间,它们都统称方法区。


# 1. 方法区

从线程的角度来看运行时数据区:

创建对象各数据区域的声明:

image-20200915095045501

# 方法区理解

元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的

👉【oracle官方文档】The Structure of the Java Virtual Machine (opens new window)

《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但些简单的实现可能不会选择去进行垃圾收集或者进行压缩”。对HotSpot而言,方法区还有一个别名叫做Non-Heap(非堆),的就是要和堆分开。

  • 方法区(Method Area)同堆区一样,是各个线程共享的内存区域

  • 方法区和堆区都是在实际的物理内存空间中,可以是不连续的

  • 方法区的大小和堆空间一样可以动态调整或者固定

  • 方法区的大小决定了系统可以加载多少个类。 如果系统定义的类太多,可能会产生OOM

    • 加载了大量的第三方jar包

    • Tomcat部署的工程过多

JDK7及之前:java.lang.OutOfMemoryError:PermGen [永久代]

JDK8及之后:java.lang.OutOfMemoryError:Metaspae [元空间]

  • 关闭JVM就会释放方法区的内存

# 方法区的演进

JDK7及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代。

  • 使用永久代,更容易导致Java程序更容易OOM。永久代仍然使用的是Java虚拟机的内存

  • 在JDK8中,类的元数据现在存储在本地内存中,这个空间被称为元空间

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。

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


【JDK7方法区使用的是虚拟机内存,同时将静态变量和字符串常量池搬移到了堆上】

Oracle官方文档说明:

👉JEP 122: Remove the Permanent Generation (opens new window)

【JDK8将永久代改为元空间,使用的是本地内存】

  • 相对于虚拟机的内存,本地内存空间更大,更不容易出现OOM

  • 永久代、元空间二者并不只是名字变了,内部结构也调整了


# 永久代为什么要被替换为元空间

【Oracle官方解释】

【翻译】这是JRockit和Hotspot聚合工作的一部分。JRockit客户不需要配置永久生成(因为JRockit没有永久代),并且习惯于不配置永久生成。

好家伙,这解释果然是通俗易懂。收购了JRockit,为了用户方便,怎么喜欢怎么来呗!别人没有,我也不要了。。。

【解释】

  1. 因为永久代使用的是虚拟机的内存,为永久代设置空间大小是很难确定的,而元空间的大小仅受本地内存限制

  2. 对永久代进行调优是很困难的


# 方法区的设置与OOM

元数据区大小可以使用参数-XX:MetaspaceSize和-XX:MaxMetaSpaceSize指定替代永久代原有的两个参数。

默认值依赖于平台。 windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaSpaceSize的值是 -1,即没有限制。

元空间与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存

如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace


# 2. 方法区的内部结构

类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。

方法区中存储的内容:

  • 类型信息(域信息、方法信息)

  • 运行时常量池


# 类型信息

对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息:

① 这个类型的完整有效名称(全名 = 包名.类名)

② 这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类)

③ 这个类型的修饰符( public, abstract,final的某个子集)

④ 这个类型直接接口的一个有序列表

# 域信息

域信息,即为类的属性,成员变量

JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)

# 方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  1. 方法名称方法的返回类型(或void)

  2. 方法参数的数量和类型(按顺序)

  3. 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集

  4. 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)

  5. 异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引


# 3. 运行时常量池

对字节码文件反编译之后,查看常量池相关信息:

注意:这里说的是常量池,而不是运行时常量池

# 运行时常量池vs常量池

方法区中,内部包含了运行时常量池

字节码文件中,内部包含了常量池

常量池:存放编译期间生成的各种字面量与符号引用

运行时常量池:常量池表在运行时的表现形式

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

理解为字节码中的常量池Constant pool只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据,被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。

它们不是一个概念,存放的位置是不同的。一个在字节码文件中,一个在方法区中。


要弄清楚方法区的运行时常量池,需要理解清楚字节码中的常量池。

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表( Constant pool table),包括各种字面量和对类型、域和方法的符号引用。

常量池表Constant pool table:

在方活中对常量池表的符号引用

为什么需要常量池?

举例来说:

public class Solution {
    public void method() {
        System.out.println("Hello");
    }
}
1
2
3
4
5

这段代码很简单,但是里面却使用了 String、 System、 PrintStream及Object等结构。如果代码多,引用到的结构会更多!这里就需要常暈池,将这些引用转变为符号引用,具体用到时,才按需加载。

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


# 4. 代码追踪方法区的使用

通过javap-v -p反编译后查看字节码文件:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
        //操作数栈  局部变量表  方法形参
        stack=3, locals=5, args_size=1
            0: sipush      500     //将500压入操作数栈中
            3: istore_1            //将操作数栈中的500存储到局部变量表的1号槽位(0号为形参args,如果
                                     //无形产则存放(this)

            4: bipush      100     //将100放入操作数栈中
            6: istore_2            //将操作数栈中的100存储到局部变量表的2号槽位
            7: iload_1             //读取局部变量表1号Slot(槽位),将数据500压入操作数栈
            8: iload_2             //读取局部变量表2号Slot,将数据100压入操作数栈
            9: idiv                //先出栈,对栈中500和100做除法,将结果5再存入栈中(500和100不再进栈)
            10: istore_3           //操作数栈中的5存储到局部变量表的3号槽位
            11: bipush      50     //定义一个int类型的变量值为50
            13: istore      4      //存储到局部变量表的4号槽位
            15: getstatic   #2     //符号引用做赋值(#31,#32,#33)
            18: iload_3            //将局部变量表3号Slot的数取出(5)
            19: iload       4      //将局部变量表4号Slot的数取出(50)
            21: iadd               //出栈,对栈中5和50做加法,将结果55再存入栈中(500和100不再进栈)
            22: invokevirtual #3   // Method java/io/PrintStream.println:(I)V符号引用做输出
                                   // (#34,#35,#36)
            25: return             //将计算结果返回,结束方法的调用
        LineNumberTable:
            line 10: 0
            line 11: 4
            line 12: 7
            line 13: 11
            line 14: 15
            line 15: 25
        LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      26     0  args   [Ljava/lang/String;
                4      22     1     x   I
                7      19     2     y   I
                11      15     3     a   I
                15      11     4     
b   I

SourceFile: "Solution.java"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

【详解】:

内存操作图解

  1. 将500压入操作数栈中

  1. 将操作数栈中的500存储到局部变量表的1号槽位(0号为形参args,如果无形产则存放(this)

  1. 将100放入操作数栈中

  1. 将操作数栈中的100存储到局部变量表的2号槽位

  1. 读取局部变量表1号Slot(槽位),将数据500压入操作数栈

  1. 读取局部变量表2号Slot,将数据100压入操作数栈

  1. 先出栈,对栈中数据500和100做除法,将结果5再存入栈中(500和100不再进栈)

  2. 操作数栈中的5存储到局部变量表的3号槽位

  1. 定义一个int类型的变量值为50

  1. 存储到局部变量表的4号槽位

  1. 真正在执行时,将符号引用转为直接引用

  1. 将局部变量表3号Slot的数取出(5)

  1. 将局部变量表4号Slot的数取出(50)

  1. 出栈,对栈中数据5和50做加法,将结果55再存入栈中(500和100不再进栈)

  1. 符号引用做输出(#34,#35,#36)

  1. 将计算结果返回,结束方法的调用

# 5. 方法区的垃圾回收

在大量使用 反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

# 频繁的使用反射

频繁的使用反射,会导致方法区加载大量的类信息。而方法区是存储在堆上、GC频率很低的区域。

回收方法区会触发Full GC,会导致STW长时间的停顿,程序的执行效率必然降低!

所以反射不能够频繁的使用!

编辑 (opens new window)
上次更新: 2021/06/27, 10:49:09
堆
对象实例化的内存布局与访问定位

← 堆 对象实例化的内存布局与访问定位→

最近更新
01
匿名内部类
10-08
02
函数式接口
10-08
03
ARouter-Kotlin踩坑
10-05
更多文章>
Theme by Vdoing | Copyright © 2021-2023 iqqcode | MIT License | 备案号-京ICP备2021028793号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×