【JVM】运行时数据区初探【方法区、字符串常量池】

运行时数据区初探

运行时数据区概述

我们可以根据是否共享分为线程共享和独有两个分类

  • 线程独享(不需要垃圾回收,随着线程创建而分配,销毁而回收):虚拟机栈、本地方法栈、程序计数器

  • 线程共享(GC 活动区域): 堆、方法区

JVM的运行时数据区的使用顺序 :

线程共享的两块数据区是在JVM启动的时候就按照参数分配好了内存,

而线程独有的三块数据区是随着线程的创建后再开辟的内存空间

JVM启动的时候

  • 先使用方法区(存类的元信息和静态变量)

    • 一般建议初始大小和最大大小设置为一样,好处是减少内存自动扩容带来的性能损耗

  • 堆:(存字符串常量池中的字符串对象)

    • JDK 1.7 后才有此使用

程序执行的时候

  • 方法区:(方法信息) 

  • 栈、程序计数器、本地方法栈

    • 和方法执行有关系

  • 堆(存储对象和数组信息)

Hotspot运行时数据区

画图说明一下他们的使用顺序【1.8做为分界点】

  • JDK 1.7 之前 对于方法区的实现称之为 :"永久代"

  • JDK 1.8 之后 对于方法区的实现称之为:"元空间"

  • 方法区是Java虚拟机规范中的一种规范,而永久代和原空间是不同版本的不同实现

分配JVM内存空间

分配堆大小

  • –Xms(堆的初始容量)

  • -Xmx (堆的最大容量)

分配方法区之永久代的大小(建议相同)

  • -XX:PermSize 永久代的初始容量

  • -XX:MaxPermSize 永久代的最大容量

分配方法区之元空间区大小

  • -XX:MetaspaceSize

    • 元空间的初始大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整

    • 如果释放了大量的空间,就适当降低该值

    • 如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值

  • -XX:MaxMetaspaceSize

    • 最大空间,默认是没有限制的

  • -XX:MinMetaspaceFreeRatio

    • 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

  • -XX:MaxMetaspaceFreeRatio

    • 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

分配线程空间的大小(也就是线程不共享的三个数据区域的容量大小)

  • -Xss :为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M

方法区

方法区存储内容

存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等等【JDK 1.7】

类型信息

  • 类型的全限定名、超类的全限定名、直接超接口的全限定名、类型标志(该类是类类型还是接口类型) 类的访问描述符(public、private、default、abstract、final、static)

类型的常量池

  • 存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用

  • 常量池中每一个保存的常量都有一个索引,就像数组中的字段一样

  • 因为常量池中保存着所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)

字段信息

  • 字段修饰符(public、protect、private、default) 、字段的类型 、字段名称

方法信息:方法信息中包含类的所有方法,每个方法包含以下信息

  • 方法修饰符 、方法返回类型 、方法名 、方法参数个数、类型、顺序等 、方法字节码 操作数栈和该方法在栈帧中的局部变量区大小 、异常表

类变量(静态变量)

  • 指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定

指向类加载器的引用

  • 每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到

指向Class实例的引用

  • 类加载的过程中,虚拟机会创建该类型的Class实例 ,方法区中必须保存对该对象的引用

  • 通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象

方法表

  • 为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组

  • 数组的每个元素是实例可能调用的方法的直接引用

  • 包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的

运行时常量池

  • 用于存放编译器生成的各种字面常量符号引用

    • 双引号引起来的字符串,比如:"鞋破露脚尖儿"
    • final修饰的变量,或者非final修饰但是是long、double、float类型的变量
    • 类的符号引用、方法、字段等
  • 这部分内容被类加载后进入方法区的运行时常量池中存放

  • 运行时常量池相对于Class文件常量池的另外一个特征具有动态性

  • 可以在运行期间将新的常量放入池中(典型的如String类的intern()方法)

永久代和元空间的区别

方法区是抽象出来的概念 JDK 1.7 及以前具体的实现是:永久代 JDK 1.8后的具体实现为:元空间

JDK 1.7的永久代所使用的内存是JVM进程所分配的内存,其大小受JVM限制
JDK 1.8的元空间所使用的内存是物理内存区域,只受物理内存大小的限制

JDK 1.7的永久代储存的基本都是在上面的图片中方法区中的数据
JDK 1.8的元空间只储存类的元信息,二静态变量和运行时常量池都被移动到了堆中

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

  • Sun公司被Oracle收购,Oracle做出的更新

  • 字符串存在永久代中,容易出现性能问题和永久代内存溢出

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,容易导致老年代溢出

    • 元空间依赖本地内存不会存在这个问题

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

方法区异常演示

类加载导致OOM异常

  • 我们现在通过动态生成类来模拟方法区的内存溢出

    • 利用不同的类加载器加载类,导致内存溢出的演示

public class OomMock{
    public static void main(String[] args) {
        URL url = null;
        List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
        try {
            url = new File("/tmp").toURI().toURL();
            URL[] urls = {url};
            while (true){
                ClassLoader loader = new URLClassLoader(urls);
                classLoaderList.add(loader);
                //每个ClassLoader对象,对同一个类进行加载,会产生不同的Class对象
                loader.loadClass("com.test.demo.memory.OomTest");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在JDK 1.7 上测试

  • 指定方法区的永久代初始和最大容量为 8m

    • java -XX:PermSize=8m -XX:maxPermSize=8m OomMock.class
    • 方法区内存溢出:java.lang.OutOfMemoryError: PermGen space

在JDK 1.8上测试

  • 因为JDK1.8 用元空间替换了永久代,所以我们的运行参数需要更改一下

  • 指定元空间的容量大小也为8m

    • java -XX:MetaSpaceSize=8m -XX:MaxMetaSpaceSize=8m OomMock.class
    • 元空间内存溢出:java.lang.OutOfMemoryError: Metaspace

字符串导致OOM异常

public class StringOomMock {
    static String base = "string";
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i=0;i< Integer.MAX_VALUE;i++){
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}
  • 可能很多人看不懂上面的一句代码:str.intern()

    • 编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用

    • 以后再调用String的intern方法,会先对String内容在常量池中做一个过滤,

    • 如果常量池中有该值,则直接返回引用地址

    • 如果常量池中无改值,则在常量池中创建一个字符串,再将引用指向

  • 上面这这样写的目的就是:不断生成新的字符串,在常量池中疯狂占用空间,促进内存溢出

JDK 1.6 的运行结果

  • java -XX:PermSize=8m -XX:maxPermSize=8m -Xms16m StringOomMock.class

    永久代内存溢出:java.lang.OutOfMemoryError: PermGen space

JDK 1.7 的运行结果

  • java -XX:PermSize=8m -XX:maxPermSize=8m -Xms16m StringOomMock.class
  • 堆内存溢出:java.lang.OutOfMemoryError:Java heap space

  • 结论:JDK 1.7 已经将字符串常量由永久代转移到堆中

JDk 1.8 的运行结果

  • java -XX:PermSize=8m -XX:maxPermSize=8m -Xms16m StringOomMock.class
  • 堆内存溢出:java.lang.OutOfMemoryError:Java heap space

  • 两个警告:PermSize=8m、maxPermSize=8m已经无效

  • 结论:JDK 1.8 中已经不存在永久代的结论

字符串常量池

又叫全局字符串常量池,全局只有一个存在,唯一的,被所有类共享

在这里我知道大家已经已经有点模糊了,现在是有几个池子啊?

  • 【Class常量池、运行时常量池、字符串常量池】

他们各自的作用是什么

  • class常量池在上面说了,

    • 用于存放类编译时产生的各种字面常量符号引用

    • 每个类都有一个

  • 运行时常量池

    • jvm在编译某个java文件的时候,就会生成class常量池

    • 当编译完成后,JVM读取该class文件到内存中时,就会将class常量池中的内容存放到运行时常量池中

      • class中的符号引用在运行时常量池中转存为直接引用

    • 每个类都有一个

    • JDK 1.6 及以前:运行时常量池位于方法区中,也就是永久代中

    • JDK 1.7 及以后,运行时常量池位于Java 堆中

  • 字符串常量池

    • 全局只有一个存在,唯一的,被所有类共享

    • 具体的下面详说......

储存了哪些内容

  • 在类加载完成,经过验证,准备阶段之后在堆中开辟的一块空间生成字符串对象实例

  • 只有双引号括起来的字符串的对象实列的引用才会被存放到字符串常量池中

  • 【记住,字符串常量池中,存的仅仅是引用,而不是实列对象】

为了更快的超找某个字符串在字符串常量池中是否存在,在设计字符串常量池的时候,设计了一个StringTable,有点类似于HashTable,里面保存了字符串的引用

在JDK 1.7往后,都可以设置StringTable的长度

  • -XX:StringTableSize=99991

  • 链表越长,占用的空间及越大,查询性能越高

  • 链表过短,hash冲突频现至于为什么会这样,下面听我娓娓道来!!!

StringTable采用了数组+链表的方式进行数据的储存,如下

  • 上如的图中,我们设置的StringTableSize为7

  • 假如现在我们现在面对一个代码:String str = "鞋破露脚尖儿";

  • 首先JVM会得到 "鞋破露脚尖儿"的hsshacode值,然后对其值对HashTableSize也就是7 进行取余

  • 得到余数,确定他的位置在上图数组的某一个索引的位置

  • 如果该位置有数据【遍历链表匹配hash】,hash匹配上直接返回对应的字符串的引用地址即可

  • 如果没有找到数据则将字符串封装到Entity对象中,进行保存到指定下标的位置上

  • 如果下标已经被其他数据占用,用链表的方式继续保存

  • 因为这个HashTable一共就7个元素,所以非常容易导致很多字符串的HashCode对7取余的值相同

  • 所以为了避免hash冲突的情况,StringTable使用上图的结构:数组 + 链表

  • 链表是增删快,查询慢,所以想要查询夸,那么我们就应该增加StringTableSize

  • 只有足够多的数组下标,才会减少hash冲突的几率,这样每个数组下标的链表上的数据就会变少

  • 这样检索速度就会提高了

注意点

1、单独使用””引号创建的字符串都是常量,编译期就已经确定存储到String Pool中 2、使用new String(“”)创建的对象会存储到heap中,是运行期新创建的 3、使用只包含常量的字符串连接符如”aa”+”bb”创建的也是常量,编译期就能确定已经存储到StringPool中 4、使用包含变量的字符串连接如”aa”+s创建的对象是运行期才创建的,存储到heap中 5、运行期调用String的intern()方法可以向String Pool中动态添加对象

字符串常量池案列分析

String str1 = "abc";
System.out.println(str1 == "abc");
  1. 栈中开辟一块空间存放引用str1

  2. String池中开辟一块空间,存放String常量"abc"

  3. 引用str1指向池中String常量"abc"

  4. str1所指代的地址即常量"abc"所在地址,输出为true

String str2 = new String("abc");
System.out.println(str2 == "abc");
  1. 栈中开辟一块空间存放引用str2

  2. 堆中开辟一块空间存放一个新建的String对象"abc"

  3. 引用str2指向堆中的新建的String对象"abc"

  4. str2所指代的对象地址为堆中地址,而常量"abc"地址在池中,输出为false

String str2 = new String("abc");
String str3 = new String("abc");
System.out.println(str3 == str2);
  1. 栈中开辟一块空间存放引用str3

  2. 堆中开辟一块新空间存放另外一个(不同于str2所指)新建的String对象

  3. 引用str3指向另外新建的那个String对象

  4. str3和str2指向堆中不同的String对象,地址也不相同,输出为false

String str4 = "a" + "b";
System.out.println(str4 == "ab");
  1. 栈中开辟一块空间存放引用str4

  2. 根据编译器合并已知量的优化功能,池中开辟一块空间,存放合并后的String常量"ab"

  3. 引用str4指向池中常量"ab"

  4. str4所指即池中常量"ab",输出为true

final String s = "a";
String str5 = s + "b";
System.out.println(str5 == "ab"); //true
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2; //StringBuffer采用toString()方法创建一个新的String对象
System.out.println(str6 == "ab"); //false
String str7 = "abc".substring(0, 2); //substring()方法创建一个新的String对象
System.out.println(str7 == "ab"); //false
String str8 = "abc".toUpperCase(); //toUpperCase()方法创建一个新的String对象
System.out.println(str8 == "ABC"); //false

String.intern()方法

intern的作用是把new出来的字符串的引用添加到stringtable中 java会先计算string的hashcode,查找stringtable中是否已经有string对应的引用了

  • 如果有返回引用(地址)

  • 如果没有就把字符串的地址放到stringtable中,并返回字符串的引用(地址)

来看看效果吧

如果你不明白为什么,听我细细道来:

  • 首先说 "java" 这个字符串的结果解析

    • "java" 这个字符串是一个特殊字符串,默认在是字符串常量池中存在的

    • String f = new String("ja") + new String("va") 就等于 String f = new String("java")

    • f.intern()发现该字符串在字符串常量池中存在,故而返回已经存在引用地址,

    • 所以 f 的引用地址并没有存放到字符串常量池中,而是指向的堆中的new String("java")

    • 所以不等,为false

  • 下面我们继续说 "joqhnq" 的细节

    • String e = new String("joq") + new String("hnq") 就等于 String f = new String("joqhnq")

    • e.intern()发现该字符串在字符串常量池中并不存在,于是将e的引用存放到StringTable中

    • 于是两者都使用的是字符串常量池中的引用,故而相等为:true

可能大家的疑惑都在下面我框起来的部分,那么下面再来思考一下:

String s3 = new String("1") + new String("1");

这段代码中,字符串常量池中到底会不会包含"11"这个字符串呢???

String s3 = new String("1") + new String("1");
String s4 = "11";
System.out.println(s3 == s4); //false

可以发现:字符串常量池中并没有包含 "11" 这个字符串

我们使用intern() 继续下探,如下所示:

String s3 = new String("1") + new String("1");
String s4 = "11";
//System.out.println(s3 == s4); //false
String s5 = s3.intern();
System.out.println(s3 == s4); //false ,11也和java一样,是一个特殊字符串
System.out.println(s5 == s4); //true,都是使用JVM默认提供的引用,故而相等

我们先一起来分析上面的操作

  1. 栈中开辟一块空间存放引用s3,堆中开辟一块空间存放一个新建的String对象"11"

  2. 栈中开辟一块空间存放引用s4,池中开辟一块空间,存放的String常量"11"

  3. 栈中开辟一块空间存放引用s5,池中想存放的String常量"11" ,发现已经存在,故而直接返回s4的引用

  4. s3 == s4 ? 一个是堆中的引用一个是字符串常量池中的引用,故而为:false

  5. s5 == s4 ?s4 和 s5 指向的都是字符串常量池中指向"11"的引用,故而为:true

.

 

posted @ 2019-05-25 13:31  鞋破露脚尖儿  阅读(515)  评论(0编辑  收藏  举报