JVM-方法区

方法区

JAVA技术交流群:737698533

方法区是运行时数据区的最后一个内容,Method Area

栈,堆,方法区中的交互关系


方法区简述

方法区(Method Area),与java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译后的代码缓存等数据

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

所以,方法区看作是一块独立于堆的内存空间

方法区在JVM启动时候就被创建,并且它的实际物理内存空间和java堆区一样都是可以不连续的,方法区的大小可以选择固定大小,也可以选择可扩展,方法区的大小决定了可以保持多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出异常:java.lang.OutOfMemoryError:PermGen Space或java.lang.OutOfMemoryError,为什么是两种异常呢?因为在JDK8之前,方法区又称为"永久代",而JDK8以及8之后改为"元空间"

方法区,元空间,永久代三者关系是什么呢?方法区是java虚拟机规范的一部分,而元空间和永久代是一个具体的实现,元空间的本质和永久代类似,都是对JVM规范方法区的实现,不过两者最大的区别就是:元空间不在虚拟机中设置内存,而是直接使用本地内存

设置方法区大小

JDK1.7及以前

  • -XX:PermSize来设置永久代初始分配空间,默认为20.75m
  • -XX:MaxPermSize来设定永久代最大可分配空间,32位机器默认64m,64位机器默认82m
  • 当jvm加载类信息超过最大容量,会报OutOfMemoryError:PermGen Space

JDK1.8以及后

  • 元数据区大小使用参数-XX:MetaSpaceSize和-XX:MaxMetaspaceSize指定,来替代jdk7原有的两个参数
  • 默认值依赖于平台,windows下默认初始化大小为21M,最大值为-1,及没有限制
  • 于永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据区发生移除,虚拟机一样也会抛出异常OutOfMemoryError:Metaspace
  • -XX:MetaspaceSize:设置初始的元空间大小,对于一个64位的服务器端JVM来说,其默认的内存大小为21MB,这就是初始的高水位线,一旦触及这个水位线,Full GC将会触发并卸载没有用的类(即这些类对应的类加载器不在存活),然后这个高水位线会重置,新的高水位线取决于GC后释放了多少元空间,如果释放的空间不足,那么在不超过MaxMetaspaceSize的情况下适当提高该值,如果释放空间过多,则适当降低该值
  • 如果初始化高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收日志可以观察到Full GC多次调用,为了避免频繁GC,建议将-XX:MetaspaceSize设置为一个相对较高的值

方法区内部结构

类型信息

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

  1. 类型的完整有效名称,全限定名
  2. 类型直接父类的全限定名
  3. 类型的修饰符(public,abstract,final的某个子集)
  4. 类型直接接口的一个有序列表

域(Field)信息 (属性,字段)

  • JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
  • 域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

方法(Method)信息

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

  • 方法名称
  • 方法的返回类型(或void)
  • 方法参数的属性和类型,按照顺序
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的子集)

non-final的类变量

静态类变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分,类变量被类所有实例共享,即使没有类实例时也可以访问

全局常量(static final)

被声明为final的类变量处理方式则不同,每个全局变量在编译的时候就会被分配了

运行时常量池

  • 运行时常量池(Runtime Constant Pool) 是方法区的一部分

  • 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期间生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

  • 当类和接口加载到虚拟机后,就会创建对应的运行时常量池

  • JVM为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的

  • 运行时常量池中包含多种不同的常量,包括编译期间就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或字段引用,此时不再是常量池中的符号地址,这里转换为真实地址

    • 运行时常量池对比Class文件的常量池的另一重要特性是具备动态性
  • 运行时常量池类似于传统编程语言的符号表,但是它所包含的数据却比符号表要更加丰富一些

  • 当创建接口或类的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区能提供的最大空间,则JVM会抛出OutOfMemoryError异常

方法区的演进细节

只有HotSpot才有永久代,HotSpot中方法区的变化:

jdk1.6即之前:有永久代(permanent generation),静态变量存放在永久代上

jdk1.7: 有永久代,但是已经逐步"去永久代",字符创常量池,静态变量移除,存放在堆空间中

jdk1.8及以后:无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符创常量池,静态变量仍在堆空间

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

  1. 永久代设置空间大小难以确定,如果设置比较小容易发生FullGC影响程序性能,而且容易出现OOM,如果过大又占用内存
  2. 对永久代的调优是很困难的

判断一个常量是否"废弃"是相对简单的,而要判断一个类型是否属于"不再被使用的类"的条件就比较苛刻了,需要同时满足3个条件

  1. 该类所有实例都已经被回收,也就是java堆中不存在该类以及任何派生子类的实例
  2. 加载该类的类加载器已经被回收,这个条件除非精心设计的可替换类加载器的场景,如OSGi,JSP的重加载等,否则通常很难达成
  3. 对应该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

java虚拟机被允许对满足上述3个条件的无用类进行回收,这里说仅仅是"被允许",而不是和对象一样,没有引用了就必然进行回收,关于是否要对类型进行回收,HotSpot虚拟机提供了-Xonclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading,-XX:+TraceClassUnLoading查看类加载和卸载信息

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

StringTable为什么调整

jdk7中将StringTbale放到了堆空间,因为永久代中回收频率很低,在Full GC 时候才会回收,而Full GC是老年代的空间不足,永久代空间不足时才会触发,这就导致StringTable回收频率不高,而我们开发中会有大量字符串创建,回收效率低导致永久代内存不足,放到堆里能及时回收内存

对象实例化内存布局和访问定位

对象的实例化步骤

  1. 判断对象对应的类是否加载,链接,初始化
    1. 虚拟机遇到一条new指令,首先去检查这个指令的参数能否在MetaSpace的常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载,解析,初始化(即判断这个类元信息是否存在),如果没有,那么在双亲委派机制模式下,使用当前类加载器ClassLoader+包名+类名为key进行查找对应的class文件,如果没有找到文件,则抛出ClassNotFontException异常.如果找到,则进行类加载,并生成对应的Class类对象
  2. 为对象分配内存,首先计算对象占用空间的大小,接着在堆空间划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量即可,即4字节大小
    1. 如果内存规整-指针碰撞:如果内存是规整的,那么虚拟机将采用指针碰撞来为对象分配内存,意思是所有用过的内存放在一边,空闲内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就是将指针向空闲内存方向移动与对象大小相同的距离,如果垃圾收集器选择的是Serial,ParNew这种基于压缩算法的,虚拟机采用这种分配方式,一般带有compact(整理)过程的收集器,使用指针碰撞
    2. 如果内存不规则-空闲列表分配:如果内存不是规整的,已经使用和未使用的内存相互交错,那么虚拟机采用的是空闲列表法来为对象分配内存,虚拟机维护一个列表,记录上那些内存块是可用的,在分配时从表中找到一块足够大的空间来分配对象实例,并更新表上的内容,这种分配方式称为"空闲列表(Free List)"
    3. 至于选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
  3. 处理并发安全问题
    1. 采用CAS失败重试,区域加锁保证更新的原子性
    2. 每个线程设预先分配一块TLAB
  4. 初始化分配到的空间
    1. 所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
  5. 设置对象头
    1. 将对象的所属类(即类的元数据信息),对象的HashCode和对象的GC信息,锁信息等数据存储在对象头中,这个过程具体设置方式取决于JVM实现
  6. 执行init方法进行初始化
    1. 在java程序视角来看,初始化才正式开始,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量,因此一般来说(由字节码是否跟随有invokespecial指令所决定),new指令之后会紧接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来

对象内存布局

对象头

  1. 运行时元数据(Mark Word)
    1. 哈希值(HashCode)
    2. GC分代年龄
    3. 锁状态标志
    4. 线程持有的锁
    5. 偏向线程ID
    6. 偏向时间戳
  2. 类型指针
    1. 指向类元数据InstanceKlass,确定该对象所属类型
  3. 如果是数组,还需要记录数组长度

实例数据(InstanceData)

它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)

  1. 相同宽度的字段被分配在一起
  2. 父类中定义的变量会出现在子类之前
  3. 如果CompactFields参数为true(默认为true),子类的窄变量可以插入到父类变量的空隙

对齐填充(Padding)

不是必须,也没有特别含义,仅仅起到占位符的作用

public class Customer {
    int id = 100;
    String name;
    Account account;

    {
        name = "小明";
    }

    public Customer() {
        account = new Account();
    }

}

class Account {

}
//=========
public class Demo {

    public static void main(String[] args) {
        Customer c = new Customer();
    }
}

对象访问方式

由于reference类型在java虚拟机规范中并没有定义这个引用应该通过什么方式去定位,所以对象访问方式也是由虚拟机自己决定的,主流的访问方式主要有两种:直接指针和句柄访问

句柄访问

直接指针

这两种方式各有优势,使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾回收时移动对象是非常普遍的行为)时只需要修改句柄的实例指针,而reference本身不需要修改,而使用直接指针访问的最大好处就是快,因为少一次指针定位的时间开销,在HotSpot虚拟机中采用的是直接指针

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中的内存区域,在JDK1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提升性能,因为避免了在java堆和native堆中来回复制数据,直接内存分配不会受到java堆大小的印象,但是既然是内存,则肯定会受到本机内存大小的限制,如果内存区域大于物理内存限制,则会抛出OOM异常

直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认大小与堆的最大值-Xmx参数值一致

posted @ 2021-02-10 17:45  Jame!  阅读(1517)  评论(1编辑  收藏  举报