JVM (六)方法区

578 阅读15分钟

目录

1.方法区的概念

方法区中主要储存类型信息,常量,静态变量,即时编译器编译后的代码缓存

1.方法区(Method Area)与Java堆一样,是各个线程共享的内存区域

2.方法区在JVM启动时就会被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的

3.方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展,方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误: java.lang.OutOfMemoryError:PermGen space 或者 java.lang,OutOfMemoryError:Metaspace,比如:

  • 加载大量的第三方jar包;
  • Tomcat部署的工程过多;
  • 大量动态生成反射类;

4.关闭JVM就会释放这个区域的内存。

2.方法区的演变

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

本质上,方法区和永久代并不等价。仅是对hotSpot而言的。《java虚拟机规范》对如何实现方法区,不做统一要求。

  • 使用的永久代缺点:使用JVM内存,导致Java程序更容易OOM(超过-XX:MaxPermSize上限)。

  • 在jdk8中,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存(PC内存)。

永久代、元空间并不只是名字变了。内部结构也调整了。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常.

2.1 方法区的变化

HotSpot中方法区的变化:

jdk1.6及以前,有永久代,静态变量存放在永久代

jdk1.7 有永久代,但逐步的去永久代,字符串常量池,静态变量被移除,保存在堆中

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

方法区常量池运行时常量池静态变量
jdk6及以前 永久代方法区方法区方法区
jdk7及以前 永久代方法区
jdk8及以前 元空间方法区方法区

2.2变化的原因

随着Java8的到来,HotSpot VM中再也见不到永久代了。但是这并不意味着类.的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间( Metaspace )。 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。 这项改动是很有必要的,原因有:

  • 1.因为永久代设置空间大小是很难确定的。 在某些场景下,如果动态加载类过多,容易产生Perm区(永久代)的O0M。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。"Exception in thread' dubbo client x.x connector’java.lang.OutOfMemoryError: PermGenspace"而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
  • 2.对永久代进行调优是很困难的。

3.方法区的内部结构

《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的 类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

3.1类型信息

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

  • 这个类型的权限定类名(全名=包名.类名)
  • 这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类)
  • 这个类型的修饰符(public, abstract, final的某个子集)
  • 这个类型直接接口的一个有序列表

3.2域信息(成员变量)

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

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

3.3 方法信息(method)

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

  • 方法名称。
  • 方法的返回类型(或void)。
  • 方法参数的数量和类型(按顺序)。
  • 方法的修饰符(public, private, protected, static, final, synchronized, native , abstract的一个子集)。
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native 方法除外)。
  • 异常表( abstract和native方法除外),每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。

3.4non-final的类变量(非声明为final的static静态变量)

静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分

类变量被类的所有实例所共享,即使没有类实例你也可以访问它。以下代码不会报空指针异常:

public class MethodAreaTest {
    public static void main(String[] args) {
        Order order = null;
        order.hello();
        System.out.println(order.count);
    }
}

class Order {
    public static int count = 1;
    public static final int number = 2;

    public static void hello() {
        System.out.println("hello!");
    }
}

3.5 常量值

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

复习:

1.clinit()即“class or interface initialization method”,注意他并不是指构造器init()

2.此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

3.如果没有静态变量,那么字节码文件中就不会有clinit方法

3.6常量池

  • 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Poo1 Table),包括各种字面量和对类型域和方法的符号引用。
  • 一个 java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池;而这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池.
  • 比如如下代码,虽然只有 194 字节,但是里面却使用了 string、System、Printstream 及 Object 等结构。
Public class Simpleclass {
public void sayhelloo() {
    System.out.Println (hello) }
}
  • 字节码当中的常量池结构(constant pool),可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息。

3.7运行时常量池

  • 运行时常量池( Runtime Constant Pool)是方法区的一部分。用于存放每个类运行时所需的引用信息

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

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

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

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

  • 运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。

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

4.方法区参数的设置

方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。

  • jdk7及以前(永久代): -XX:PermSize来设置永久代初始分配空间。默认值是20.75M -XX : MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M

当JVM加载的类信息容量超过了这个值,会报异常OutOfMemoryError : PermGen space

  • jdk8及以后(元空间): 元数据区大小可以使用参数-XX:MetaspaceSize和-XX :MaxMetaspaceSize指定,替代上述原有的两个参数。

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

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。 如果元数据区发生溢出,虚拟机一样会拋出异常OutOfMemoryError: Metaspace。

-XX:MetaspaceSize: 设置初始的元空间大小。对于一个64位的服务器端JVM来说, 其默认的-XX :MetaspaceSize值为21MB.这就是初始的高水位线,一旦触及这个水位线,Full GC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。 如果初始化的高水位线设置过低,.上 述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁地GC,建议将- XX :MetaspaceSize设置为一个相对较高的值。

5.常量池

在Java的内存分配中,总共3种常量池:

5.1.字符串常量池(String Constant Pool):

5.1.1:字符串常量池在Java内存区域的哪个位置?
  • 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
  • 在JDK7.0版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。
5.1.2:字符串常量池是什么?
  • 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。
  • 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在JDK7.0中,StringTable的长度可以通过参数指定:
-XX:StringTableSize=666661

String#intern() 方法,这个方法的作用就是:

  • 如果字符串未在 Pool 中,那么就往 Pool 中增加一条记录,然后返回 Pool 中的引用。
  • 如果已经在 Pool 中,直接返回 Pool 中的引用。

只要 String Pool 中的 String 对象对于 GC Roots 来说不可达,那么它们就是可以被回收的。

如果 Pool 中对象过多,可能导致 YGC 变长,因为 YGC 的时候,需要扫描 String Pool,

5.1.3:字符串常量池里放的是什么?
  • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
  • 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用
  • 需要说明的是:字符串常量池中的字符串只存在一份!如:
String s1 = "hello,world!";
String s2 = "hello,world!";

即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。

5.2.class常量池(Class Constant Pool):

5.2.1常量池的演变
  • Java6和6之前,常量池是存放在方法区(永久代中的。

  • Java7,将常量池是存放到了堆中

Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中

5.2.1:class常量池简介
  • 我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
  • 每个class文件都有一个class常量池。
5.2.2:什么是字面量和符号引用
  • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  • 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。

5.3.运行时常量池(Runtime Constant Pool)

  • 运行时常量池存在于方法区中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用
  • JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

6.StringTable

jdk1.8底层使用char[]

jdk1.8之后底层使用byte[]

String 是堆空间中存储数据的主要对象,而且大部分拉丁字符只需要使用一位byte数组就可以进行储存,如果使用char数据存储,每个字符采用两个byte数组,会造成内存空间的浪费,所以对拉丁字符采用一个字节进行存储,而对于汉字依旧采用两个字节,通过字符编码的标识来进行区分

6.1String的基本特性

  • 对字符变量进行重新赋值时,需要在字符串常量池中进行重新生成

  • 字符串常量池中不会存储相同的字符串

    string pool 是一个固定大小的hashtable,jdk1.6默认大小为1009,jdk1.7默认为60013,jdk1.8设置的最小值为1009

    通过-XX:StringTableSize来设置大小

6.2string的内存分配

jdk6及以前,放在方法区

jdk7开始放在了堆区的老年代中

因为字符串常量池放在方法区,方法区的默认大小较小,且不方便进行GC,GC频率太低

  • 直接使用双引号声明出的String对象会直接存储在常量池中
  • 使用String提供的intern()方法,直接加载进内存

6.3 字符串的拼接操作

字符串与字符串的拼接结果直接存放在常量池中,经过编译期优化,会在字符串常量池中生成结果字符串

只要拼接的字符串中存在变量,则需要在堆空间中new String()新的对象,具体的内容为拼接的结果

String s1="a";   //第一个对象
Strinh s2="b";   //第二个对象
String s3=s1+s2;  //两个对象
//StringBuilder s=new StringBuilder()
//s.append("a")
//s.append("b")
//s.toString() ------>  约等于  new String("ab")  //toString 方法的调用不会在字符串常量池中生成ab,new  String("ab")会在字符串常量池中生成ab
在jdk5.0之前使用StringBuffer,jdk5.0之后使用Stringbuilder

通过StringBuilder添加字符的方式远高于字符串的拼接方式

6.4intern()的使用

如何保证变量S指向的是字符串常量池中的数据

方式一:String s = "abc";

方式二:String s = new String("abc").intern();

String s = new StringBuilder("abc").toString().intern();

6.5new String("ab")会创建几个对象

两个对象,一个是new String对象,一个是常量池中的字符串“ab”对象