JVM-方法区


前言

方法区是最重要的三个区域(堆、栈、方法区)之一。堆、栈、方法区都是在JVM启动时创建的,方法区既和堆相似,又有较为严格的区分。
在官方的描述的,方法区被称为“Non-Heap”(非堆),目的是为了与堆区进行区分。

方法区具有OOM 和GC,但是由于Metaspace较为固定,且常常不设置上限(-1),因此不容易出现OOM。

方法区主要用来存类型信息、方法描述信息、静态变量信息等。【类/型信息,注意断句】

下面的图片表现了JVM运行时,需要加载的类数量。

加载的类的数量:
即使只有几行代码【十几行,算很短了】,也加载了1600多个类:
在这里插入图片描述

在这里插入图片描述

成员变量是随着对象放在堆中的;
局部变量放在栈中栈帧的局部变量表中
静态变量放在堆中【jdk7之后,之前是在永久区】,目的是为了方便回收;
对象实例总是放在堆中的,
StringTable在jdk7之后放在堆中。

对象实例 != 对象;
对象是指引用,实例是指堆中的实际内容。

一、方法区与栈、堆的配合

在一行简单的代码中,可以看到方法区、栈、堆各种的作用:
Person person = new Person();
这一行代码中,方法区负责第一个单词的作用,即Person,在方法区中,存放着Person类的描述信息,并提供给堆区构造方法<init>来创建对象

栈区负责第二个单词,即person,的作用,在栈区中,会存储person这个引用指向的堆内存指针,并将其符号引用也放在本地变量表中,提供给code调用。

堆区负责最后两个单词的作用:new关键字的含义是指去堆区创建对象,Person()表示调用方法区中Person类的<init>来创建一个Person类的实例。此实例拥有一个方法取得指针,指向方法区中Person类的类信息,以便调用方法和使用变量

上一行代码的图示。
在这里插入图片描述


从内存图示来看,方法区属于线程共享内容。
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

二、演进

Oracle的JVM规范中没有永久代的概念。这个是HotSpot自作主张开发的一种方言叫法,因为HotSpot的影响力巨大,因此这种叫法被使用者广泛使用,成为了JVM方法区的代称。

后来,Oracle收购了当时最快的JVM的厂商的虚拟机,JRockit虚拟机,JRockit使用元空间而不是永久代,不仅高效,还较少发生OOM,为了提升HotSpot性能,Oracle将JRockit的这点吸收到了HotSpot的建立上,在JDK1.8后也使用元空间代替了永久代。

永久代和元空间的区别:

  1. 永久代使用的是JVM的虚拟内存空间,元空间直接使用主机的原始内存;
  2. 元空间的内部结构较永久代有所改变;

三、参数设置

永久代:

  • 设置初始大小:-XX:PermSize=_M
  • 设置最大大小:-XX:MaxPermSize=_M

元空间:

  • 设置初始空间:-XX:MetasapceSize 默认21
  • 设置最大空间:-XX:MaxMetaspaceSize默认-1,表示无限制

在实际开发中, 不会设置上限,即保持最大空间为默认值-1;

四、内存溢出与内存泄漏

内存溢出:单纯空间不足,比如你电脑内存配置不行,你又非要跑大型游戏,就会跑不动甚至卡死,就是内存溢出;

泄漏:这是程序自身的原因,因为某一个错误的做法导致内存中某些对象不会释放,会一直占用堆空间,最后也无法释放,浪费空间

内存溢出的解决办法是提升空间,提前释放类。
内存泄漏的解决办法是用工具(如JVisual等)排查程序问题对象,进行修补。

在这里插入图片描述

五、方法区结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


case:查看字节码中信息,一个比较全面的代码:

/**
 * 查看方法区信息
 */
public class Demo2_1 {
    
//    域变量
    public int a;
    private static String msg = "msg";
    private Demo2_1 self;
    
//    构造器

    
    //    方法
    public void method1() {
        System.out.println("msg");
    }


    public static String method2() {
        StringBuilder sb = new StringBuilder();
        sb.append("HELLO");

        try {
            sb.append(", World!");
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return sb.toString();
    }

进行反编译,并输入到txt文本中:
使用terminal命令javap -v -p Demo2_1.class > text.txt 字节码文件最终会存入到方法区之中
使用Sublime打开txt文件,查看这个类的结构:

①类型信息:
在这里插入图片描述

可以看到这个类的描述符,名称,包,父类和自身类的符号引用。

②域变量信息:
在这里插入图片描述

从左上角可以看到,字节码很贴心的为整个类的区域加了个大括号。

对于变量a, 其Descriptor为I,表示为int类型, 使用权限为ACC_PUBLIC, 即access:public

同样,对String类型的变量msg也有同样的类型与权限信息,不同的是,由于是引用类型,其类型为全限定类名
尽管我在程序中给了这个变量做赋值,但是在域对象区域没有体现。

③方法信息:

一:构造方法
我是用了默认的构造方法:
在这里插入图片描述
可以看到,在方法区的描述中,构造方法**被视为了普通方法,甚至具有返回值类型V,即void
在他的本地变量表中,描述了这个方法在字节码的起始位置和长度。

在方法体部分,注释中给出他指向的位置是<init>方法的位置。

二:成员方法
在这里插入图片描述
method1()是一个成员方法,打印一个信息。
可以见到他的返回值、修饰符以及在方法中引用的静态常量池的符号引用。

三:静态方法:
在这里插入图片描述
dup:复制命令
ldc:大数赋值

本方法还多了一个行数对照表,用于查找当前字节码行在源代码方法中的位置。
由于本方法有try-catch语句,在其中有goto语句,指向发生异常时会去往的位置。

另外有一个Static块,具体应该表示static变量的初始化过程。

六、常量池与运行时常量池

常量池是一个编译时期的字节码的概念。当编译结束之后,会将源代码中的设计的类信息、对象名信息、方法描述信息、常量信息等等全部都汇总成一个符号引用表【之所以设计为引用表,是为了重用,比如System类的描述信息可能在许多语句都会用到,因此用一个代表表示就可以被不同的语句引用到】

对于字节码文件,其大小不可能太大,否则加载速度会有问题,然而在代码运行时又涉及了巨多的类和变量、方法等信息,为了能够表示这些信息,采用符号引用暂存,符号引用会指向只有简单“名称”的信息【如java.lang.String, 就用来表示String这个Class对象】,等到实际运行时,将这些符号引用对应的名字的信息都转换为实际引用。

实际引用类型有很多,具体指:

  • 堆区中对象实际地址;
  • 方法区中Class对象地址;
  • 地址偏移量等。

一个非常形象的比喻:
在执行编译时,源代码会变成一个类似“菜谱”的字节码文件。
菜谱中有各种表示做菜要用的材料名字:如,调料(Class信息),食材(各种对象、数值的引用),手法(method信息),这些信息不会具体描述,只会给出名字。


同时,为了减少写这些名字的次数,他们会为这些东西编号,如盐#1,味精#2,猪肉#27等等,这样若有多个菜都需要用到这些材料,就可以方便用代号来称呼他们了。


然而一个菜谱肯定无法真的做出菜的。
之后,厨师(执行引擎)会根据这些信息去实际的购买(转换实际地址),将空有名字的东西变成实际存在的材料,就可以进行操作了。


下图是一个类的常量池。、

点开来看,主要有两种:
第一种是有一个字符串的单元,表示类,方法,变量的名称
在这里插入图片描述
可以猜得到,这个应该是表示StringBuilder的append()方法,可见,他只会阐述一下名称字符串,而不会具体的将这个方法的内容写进去。

还有一种是符号引用,指向常量池的其他部分
在这里插入图片描述
可以猜到,#21肯定就是StringBuilder的类信息。点进去,还是一个引用,指向的这次的确是StringBuilder的包和类名。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


运行时常量池是运行时的概念,经过动态链接之后,字符引用转化为了实际引用,其中的内容切实地指向了某个内存的位置,方便执行时找到对应的区域。
在这里插入图片描述


附加一段搜到的资料:

  • java – JVM的符号引用和直接引用

在JVM中类加载过程中,在解析阶段,Java虚拟机会把类的二级制数据中的符号引用替换为直接引用。

1.符号引用(Symbolic References):

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

2.直接引用:

直接引用可以是
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

转载自(https://www.cnblogs.com/shinubi/articles/6116993.html)

七、方法区变化及原因深究

永久代是HotSpot虚拟机独有的概念。
HotSpot出于对方法区增强管控的思想,分配方法区内存时选择分配虚拟机的虚拟内存。

由于Oracle想合并JRockit虚拟机,因此将永久区替换为元空间。【官方描述,但是还是没说到替换为元空间的好处。】

需要尤其注意的是,在HotSpot的演化过程中,其静态变量与字符串常量池(或者说字符串字面值)所处的位置。
在1.6时代,静态变量存放在永久代中。
在1.7时代,出于“元空间化”思潮,将字符串常量池、静态变量移入堆区
在1.8时代,“元空间”代替永久代,字符串常量池、静态变量为了方便回收仍然放在堆区中

静态变量位置常常被弄错。

在这里插入图片描述

1.6时代。绿色背景全为虚拟机内存。
StringTable即字符串常量池。<Table有哈希表的意思>
在这里插入图片描述

1.7时代,已经移入堆中。但是还是全部内容都在虚拟机内存
在这里插入图片描述

1.8时代,引入了本地内存(黄色背景)。这时候更加体现方法区是一个JVM规范中的概念,他的实现已经不再局限于虚拟机了。
在这里插入图片描述


拒绝套娃行为:直到永久代被Pass的真正原因。

(一)永久代执行FullGC困难。
永久代 类的型信息 回收相当困难,是“吃力不讨好”的工作,因此将其中需要经常回收的字符串和静态变量放到堆区可以很方便的回收。而对不怎么需要的回收内容进行空间分配就很容易了。
(二)永久代难以调优。
在这里插入图片描述

String字面量为什么调整到堆中:增加回收效率,减少FullGC的频率【Full会连带方法区这个极难回收的一起回收。】
在这里插入图片描述

八、方法区的垃圾回收

在Java虚拟机规范中,明确指出方法区可以不具备回收能力和紧凑能力【紧凑是回收后将不连续空间聚合的技术,局部性原理使得紧凑的内容更加容易访问。】
且确实存在这种不提供回收的收集器。
在这里插入图片描述

方法区的回收主要涉及两个内容:
(一)废弃常量。这个很容易,只要没有引用指向就行。
(二)类型信息,达成条件苛刻,需要大量的检查,且回收过程容易引发Bug。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

九、运行时数据区总结及几道面试题

运行时数据区已经结束。下图是运行时数据区各部分以及指向的示意图
注意中下那个动态链接的指向:
当动态链接结束后,栈帧的常量池符号引用会变成切实的引用,指向这个栈帧的方法中的实际方法区地址。
在这里插入图片描述


面试题:【用我自己的话解答一下】

在这里插入图片描述
(1)JVM内存模型;

  • PC:记录栈帧中下一条指令地址。私有。不会OOM、GC,配合执行引擎执行程序的解释工作。【本地方法栈也会用到PC】

  • 虚拟机栈:私有
    栈帧:虚拟机栈的单元,代表当前方法,栈帧的组成有:
    局部变量表,记录当前方法中所有的局部变量,是一个表,具有下标;
    【行数对照表:映射Java代码和字节码的行数,不是栈帧内容,而是字节码内容】
    操作数栈:用于临时存储变量进行运算,深度在编译器决定,是一种栈
    返回地址:记录调用当前方法的上一个方法的位置;
    动态链接:在运行时将栈帧的符号引用转化为实际引用。

  • 本地方法栈:
    类似虚拟机栈,但是只存储本地方法。【本地方法是其他语言的程序接口,可以直接调用】

  • 堆:用于创建和存储对象,是对象创建的唯一场所
    新生代:存储刚创建的对象,分为:
    EDEN:存储生命周期短的对象,有MinorGC/YoungGC
    Survivor0/Survivor1:存储由MinorGC中幸存的对象,并为Age加1,当达到Threshold时对象晋升到老年区;
    老年区:存储生命周期长的对象和大对象,存在MajorGC/OldGC,需要区分MajorGC与FullGC

  • 方法区
    分为之前的永久代和之后的元空间。
    主要存储类型信息和方法信息、常量池信息等等,由FullGC负责回收。类加载后,字节码文件会成为Class对象存储到方法区,并且其中常量池符号引用转化为实际地址。

(2)java8内存分代改进
将永久区改为元空间。为了与JRockit的融合。
好处是:减小了调优的难度;避免了永久代的难分配空间的缺点。

(3)为什么有两个Survivor区?
只有一个Survivor区能在一个时刻存放,当经过一轮MinorGC后,Eden区和存放对象的Suivivor区的新一轮幸存者放到另一个,并且AGE++。两个Suivivor区是为了轮流存放幸存对象,方便计数以晋升

(4)Eden: Survivor0:Survivor! = 8 : 1 :1

(5)为什么要有老年代和新生代?
分代是为了减少对不必要检查的对象的检查,即长生命对象,统一对短生命对象的检查效率很高。

在这里插入图片描述
(6)为什么要分新生代、老年代、持久代,新生代为什么要有Eden与Survivor
。。。持久代存放的是几乎不会进行回收的内容,即类信息、常量信息等。
分代思想有利于更合理的进行辣鸡回收,减少不必要的存活检查与复杂GC的频次,尽量多的进行YoungGC

Eden与Survivor的划分一个是为了具有足够的空间存放新对象(Eden),二个是对GC后幸存对象进行统计,将其中存活时间长的对象转到老年区,避免检查次数,使得新老分区变得有意义。

(7)java内存分配
记不太清了。
三大原则,一大担保:

  • 尽量分配在Eden区;
  • 大对象分配在Old区
  • 长期存活对象晋升到老年区;
  • 空间分配担保【MinorGC前检查老年区空间,若不够直接进入FullGC】

(8)永久代会进行辣鸡回收吗?
会,但是回收难度巨大。会在堆内存进行FullGC时一起进行回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值