当前位置: 首页 > 科技观察

面试官:说说你对方法区的演化过程和内部结构的理解

时间:2023-03-19 19:48:38 科技观察

在了解“运行时数据区”的程序计数器、虚拟机栈、本地方法栈和堆空间之前,我们先来了解一下今天来看看最后一个模块——方法区。简介创建对象时的内存分配图《Java虚拟机规范》明确指出:“虽然所有方法区在逻辑上都是堆的一部分,但一些简单的实现可能不会选择进行垃圾收集或压缩。”虽然JavaVirtualMachine规范将方法区描述为堆的一个逻辑部分,但是它有一个别名叫做Non-Heap(非堆),目的应该是为了区别于Java堆。因此,方法区可以看作是一个独立于Java堆的内存空间。和Java堆一样,方法区是各个线程共享的一块内存区域。方法区会在JVM启动时创建,其实际物理内存空间可以是不连续的。关闭JVM将释放该区域的内存。永久代和元空间《java虚拟机规范》对于方法区如何实现没有统一的要求。例如:永久代的概念在BEAJRockit/IBMJ9中是不存在的。对于HotSpot,在jdk7及之前,习惯上将方法区的实现称为永久代,而从jdk8开始,永久代被元空间取代。方法区是Java虚拟机规范中的一个概念,永久代和元空间是HotSpot虚拟机对方法区的一种实现。通俗地说:如果把方法区比作接口,那么永久代和元空间就可以比作实现接口的实现类。直接内存永久代和元空间不仅在名称上有所改变,内部结构也进行了调整。永久代使用JVM内存,而元空间使用本地直接内存。直接内存不是JVM运行时数据区的一部分,所以不受Java堆的限制。但是会受到机器总内存大小和处理器寻址空间的限制,所以如果这部分内存也被频繁使用,还是会造成OOM错误。方法区的大小方法区的大小是可以设置的,可以选择固定大小也可以扩展。jdk7及之前-XX:PermSize=N//方法区(永久代)初始分配空间,默认值为20.75M-XX:MaxPermSize=N//方法区(永久代)最大可分配空间。32位机默认为64M,64位机默认为82Mjdk8及以后版本。空间根据运行时的应用程序需求动态调整大小。-XX:MaxMetaspaceSize=N//方法区(元空间)最大可分配空间,默认值为-1,即没有限制。与永久代最大的区别在于,如果不指定大小,随着创建的类越来越多,虚拟机会耗尽所有可用的系统内存。方法区的大小决定了系统可以保存多少类。如果系统定义的类过多,例如:加载大量第三方jar包、Tomcat部署的项目过多、动态生成的反射类过多等都会导致方法区溢出,抛出内存溢出错误。Permanentgeneration:OutOfMemoryError:PermGenspaceMetaspace:OutOfMemoryError:Metaspace至于如何解决OOM异常,会在以后的文章中讲解!jvisualvm我们可以使用JDK自带的jvisualvm工具查看程序加载的class文件:examplepublicclassMethodAreaDemo1{publicstaticvoidmain(String[]args){System.out.println("start...");try{Thread.sleep(1000000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("end...");}}运行程序,可以看到一个简单的程序需要加载so许多类文件。高水位线对于64位服务器端JVM,XX:MetaspaceSize=21是初始高水位线。一旦触及这个水位线,就会触发FullGC,卸载无用的类(即这些类??的类加载器对应的类不再存活),然后重置高水位线。新的高水位线的值取决于GC后释放了多少元空间:如果释放空间不够,则在释放空间不超过MaxMetaspaceSize的情况下适当增加该值;如果释放空间过多,则适当减小该值。如果初始高水位线设置得太低,高水位线调整可能会发生多次。通过垃圾收集器的日志可以观察到FullGC的多次调用。为避免频繁的GC,建议将-XX:MetaspaceSize设置为一个比较大的值。内部结构《深入理解Java虚拟机》书中对方法区的存储内容描述如下:用于存储虚拟机加载的类型信息、常量、静态变量、即时编译器编译的代码缓存等。接下来,我们来看看它的内部结构。类型信息对于每一个加载的类型(类类、接口接口、枚举enum、注解注解),JVM必须在方法区存储如下类型信息:该类型完整有效的名称(全名=包名.类名)此类型的直接父级的完全限定名(对于接口或java.lang.Object,没有父级)此类型的修饰符(public、abstract、final的某个子集)直接接口的有序列表该类型的字段(Field)信息JVM必须在方法区保存该类型的所有字段(fields,也叫属性)的相关信息和字段的声明顺序;字段的相关信息包括:字段名、字段类型、字段修饰符(public、private、protected、static、final、volatile、transient的某个子集)方法(Method)信息JVM必须保存所有方法的如下信息,包括声明的顺序,如领域信息:方法名称方法返回类型(或void)方法参数的数量和类型(按顺序)方法的修饰符(public、private、protected、static、final、synchronized、native、abstract)方法字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)异常表(abstract和native方法除外)每次异常处理的起始位置、结束位置、代码处理中的偏移地址程序计数器,捕获的异常类的常量池索引非最终类变量静态变量与类相关联,随着类的加载而加载,它们成为类的逻辑部分ssdata类变量是类的所有实例共享的,即使没有类实例也可以访问。我们可以通过一个例子来看:publicclassMethodAreaDemo2{publicstaticvoidmain(String[]args){Orderorder=null;order.hello();System.out.println(order.count);}}classOrder{publicstaticintcount=1;publicstaticfinalintnumber=2;publicstaticvoidhello(){System.out.println("hello!");}}运行结果为:hello!1可以打开IDEA的Terminal窗口,在MethodAreaDemo2.class所在路径下输入javap-v-pMethodAreaDemo2.class命令通过图片我们可以看出声明为final的类变量的处理方式是不一样的,和全局常量在编译时分配。运行时常量池说到运行时常量池,我们先来了解什么是常量池表。常量池表一个有效的字节码文件,不仅包含类版本信息、字段、方法、接口等描述信息,还包含一段信息,即常量池表(ConstantPoolTable),它存储的是量化值,String值、类引用、字段引用和方法引用。为什么字节码文件需要常量池?java源文件中的类和接口编译后会生成一个字节码文件。字节码文件需要数据支持,通常数据比较大,不能直接存入字节码。另一种方式,可以将指向这些数据的符号引用存放在字节码文件的常量池中,这样字节码在运行时只需要通过常量池通过动态链接找到并使用相应的数据即可。运行时常量池是方法区的一部分。类加载器在加载字节码文件时,会将常量池表加载到方法区的运行时常量池中。运行时常量池包含各种不同的常量,包括在编译时定义的数字字面量,以及只有在运行时解析后才能获得的方法或字段引用。这时候就不再是常量池中的符号地址了,取而代之的是这里的真实地址。运行时常量池相对于Class文件常量池的另一个重要特点是它是动态的,比如String.intern()。进化细节针对Hotspot的虚拟机:jdk1.6及之前:有永久代,永久代上存放静态变量;jdk1.7:有永久代,但是已经逐渐“从永久代中移除”了。静态变量被移除并存储在堆中;jdk1.8及以后:没有永久代,类型信息、字段、方法、常量存储在本地内存的元空间中,但字符串常量池和静态变量仍在堆中;evolution示例图为什么要用元空间代替永久代呢?永久代使用JVM的内存,受JVM设置的内存大小限制;元空间使用本地直接内存,其最大可分配空间为系统的可用内存。空间。因为类的元数据存储在元空间中,随着内存空间的增加,可以加载更多的类,相应的溢出概率也会大大降低。在JDK8中,合并HotSpot和JRockit的代码时,JRockit一直没有永久代。合并后就不需要设置这样的永久代了。调整永久代是很困难的。为什么要调整StringTable?因为永久代的回收效率很低,只有在fullgc的时候才会触发。FullGC只有在老年代空间不足,永久代不足时才会触发。这导致StringTable的回收效率低下。我们开发中会创建大量的字符串,回收效率低,导致永久代内存不足。放在堆中可以及时回收内存。垃圾回收相对来说,垃圾回收行为在这个区域比较少见,但在数据进入方法区后并不会“永久存在”。方法区的垃圾回收主要回收两部分:常量池中的常量和不再使用的类型。方法区的常量池主要存放两类常量:字面量和符号引用:字面量更接近Java语言层面的常量概念,比如文本字符串、声明为final的常量值等。符号引用属于编译原理的概念,包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。HotSpot虚拟机对于常量池有明确的回收策略。常量池中的常量只要没有在任何地方被引用,就可以被回收。类型判定判定一个常量是否“过时”相对简单,但判定一个类型是否属于“不再使用的类”则更为严格。需要同时满足以下三个条件:该类的所有实例都已被回收,即Java堆中没有该类的实例和任何派生子类;加载该类的类加载器已经被回收,除非这种情况是设计良好的可替换类加载器场景,如OSGi、JSP重载等,否则通常很难实现;该类对应的java.lang.Class对象没有在任何地方被引用,也不能在任何地方通过反射访问该类的方法。Java虚拟机允许回收满足以上三个条件的无用类。我这里说的只是“允许”,跟对象不一样,不引用就必然会被回收。本文转载自微信公众号“阿Q说码”,可通过以下二维码关注。转载本文请联系阿Q获取代码公众号。