JVM虚拟机之内存区域

Java中一个程序的运行,离不开jvm对内存区域的严格划分,每个区域各司其职,以此保证程序的正确运行。

一个程序从文件到运行的流程图

其中JVM中内存区域可以分为5大类。程序计数器,虚拟机栈,堆,方法区,本地方法栈。下面会依次讲解每个区域的作用。

程序计数器

作用:记住下一条指令的执行地址

特点:线程私有

存放位置:寄存器(速度极快)

是否存在内存溢出:不会,而且是唯一一个不会内存溢出的区域(Java虚拟机规范规定)

上图是程序经过编译器编译后的字节码,是一条条JVM指令,每个操作系统的指令集是一致的。这里也解释了,java一次编译,到处运行的特点。这些指令计算机是看不懂的,中间需要有解释器把这些指令转换成机器码,解释完后CPU就可以运行这些机器码指令了。

java代码 ——> 字节码 ——> jvm指令 ——> 机器码 ——> CPU读取机器码指令集,运行

虚拟机栈

含义:线程运行需要的内存空间

特点:线程私有,先进后出

组成部分:一个或多个栈桢组成,每个方法的调用,意味着开启一个栈桢。

栈桢:每个方法运行时需要的内存(局部变量,参数,返回地址)

默认大小:Linux / Mac OS  为 1M。Windows 取决于Win的虚拟内存

设置栈大小的参数:-Xss1m

每个线程调用方法后都会对应一个栈,在方法中每个线程都有一份局部变量,彼此之间是互不干扰的。

虚拟机栈的OOM有两种情况,一种栈桢过多(递归),一种栈桢过大(局部变量过多,所占用内存大于栈内存)

Eg:

package com._12306.SellTickets;

public class Test4 {

    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }

    private static void method1() {
        count++;
        method1();
    }

}

以上的demo没有设置停止条件,不断的在main方法中调用method1方法,只进不出,等到22204次时,栈已经容纳不了这么多的栈桢,遂即报出栈内存溢出的错误。 

本地方法栈

作用:为虚拟机使用到的本地方法服务(通过接口调用其他语言的方法)

功能:与虚拟机栈类似,在HotSpot虚拟机中,它将两者合二为一

特点:线程私有,会出现OOM

作用:通过new关键字创建出来的对象,都存放在堆中

特点:线程共享,存在安全问题,有垃圾回收机制清除无引用对象

分区:新生代,老年代,永久代

设置堆大小的VM参数:-Xmx2m

堆内存溢出是因为,不断的创建对象,而且这些对象还被引用,GC无法回收。随着对象的不断累积,造成堆内存耗尽,抛出异常。

Eg

package com._12306.SellTickets;

import java.util.ArrayList;
import java.util.List;

public class Test5 {
    public static void main(String[] args) {
        int i = 0; //计数器
        try {
            List<String> arraylist = new ArrayList<>(); //String对象被arrayList引用,GC无法回收
            String a = "Hello";
            while (true) {  //无限循环
                arraylist.add(a);  //不断往集合中添加字符串,每次添加是之前的两倍
                a += a;
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }

}

StringTable

作用:存放字符串的对象,提升字符串的利用率。

底层实现:Hash表来保证每个字符串的唯一。

便利:节省内存,读取更快(根据字面量很快就定位到字符串对象)

存放位置:1.6时存放在永久代,1.7以后存放在堆中。(永久代回收效率不高)

特点:线程共享,而且十分安全,一旦创建就不会改变,会出现OOM

OOM演示,设置VM参数:-Xmx10m -XX:-UseGCOverheadLimit

Eg:

package com._12306.SellTickets;

import java.util.ArrayList;
import java.util.List;

public class Test14 {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        try {
            for (int j = 0; j < 260000; j++) {
                list.add(String.valueOf(i).intern()); //将对象i 放入至(StringTable)串池中
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }

}

结果

StringTable遇到GC

Eg

package com._12306.SellTickets;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Test15 {
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        try {
            for (int j = 0; j < 40000; j++) { // j=100, j=40000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

结果

GC回收后的状态,原本需要容纳60000多个字符串对象,现在只容纳3829个,新生代进行了两次GC 。

方法区

用途:存放类的结构相关信息,成员变量,方法,类的构造器,运行常量池

存放位置:1.8以前叫做永久代,存放在堆中。1.8叫做元空间,存放在本地内存

特点:线程共享,不容易OOM

设置方法区大小的VM参数:-XX:MaxMetaspaceSize=10m

方法区内存溢出是因为产生类的字节码文件过多,其中大量使用cglib动态代理是内存溢出的主要原因。

由于1.8用的是操作系统的内存,我的电脑内存有4G,想要把它填满不是太容易。下边的例子已设置方法区的内存为10M。

Eg:

package com._12306.SellTickets;

import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

public class Test7 extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            Test7 test = new Test7();
            for (int i = 0; i < 40000; i++,j++) {
                //ClassWriter 的作用是声称类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                //版本号,public,类名,包名,夫类,接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                //返回byte[]
                byte[] code = cw.toByteArray();
                //执行类的加载
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

常量池(方法区的一部分)

含义:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量

等信息。

生成时期:被编译器编译成字节码文件时创建

位置:字节码文件中,通过命令 javap -v 字节码文件.class 可查看

Hello,World 作为初学者的第一个程序,接下来就拿它来讲解

package com._12306.SellTickets;

public class Test8 {
    public static void main(String[] args) {
        System.out.println("Hello,World!");
    }
}

由于Idea比较智能,会把二进制字节码文件重新编译成java文件,使用javap 反编译字节码文件 

HackerZhao:SellTickets apple$ javap -v Test8.class 
Classfile /Users/apple/IdeaProjects/aliossdemo/target/classes/com/_12306/SellTickets/Test8.class
  Last modified 2021-9-9; size 569 bytes
  MD5 checksum 4298cd01954f8e99de5ef1340de3ace6
  Compiled from "Test8.java"
public class com._12306.SellTickets.Test8
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello,World!
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/_12306/SellTickets/Test8
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/_12306/SellTickets/Test8;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Test8.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello,World!
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/_12306/SellTickets/Test8
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public com._12306.SellTickets.Test8();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/_12306/SellTickets/Test8;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello,World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "Test8.java"

其中Constant pool就是该进程的常量池,里面包含着各种指令信息。

上面是编译后的main方法,从0:getstatic   #2 开始执行,通过常量池表找到#2的指令,后面跟着#21,#22再通过指令#21,#22一直找到想要的信息,得知这是一条输出语句:Field java/lang/System.out:Ljava/io/PrintStream;

运行时常量池

常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

直接内存

不属于java虚拟机的内存管理,而是属于系统内存

常见于nio操作,在nio数据读写时,做缓冲区内存

分配和回收成本较高,但读写性能好

不受JVM内存回收管理

​​​​​​​

Rg

package com._12306.SellTickets;

import sun.misc.Unsafe;

import java.io.IOException;
import java.lang.reflect.Field;

/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Test19 {
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffffer 对象,一旦ByteBuffffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存

常见的性能监控,故障处理工具

场景1:CPU占用比例高

package com._12306.SellTickets;

import java.util.concurrent.TimeUnit;

public class Test6 {

    public static void main(String[] args) {
        new Thread(null,() ->{
            while(true){

            }
        },"thread1").start();

        new Thread(null,() ->{
            System.out.println("1...");
            try {
                TimeUnit.SECONDS.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"thread2").start();

        new Thread(null,() ->{
            System.out.println("2...");
            try {
                TimeUnit.SECONDS.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"thread3").start();

    }

}

运行程序,thread1因为程序的while(true)的无限循环,造成的CPU占用率居高不下。

通过 top 命令查看进程id,cpu信息,下图

通过jstack命令查看进程的详细信息

场景2: 死锁,相互等待对方的锁,谁也不肯放手。

package com._12306.SellTickets;

import java.util.concurrent.TimeUnit;

public class Test9 {
    static A a = new A();
    static B b = new B();

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            synchronized (a) {  //线程1得到锁a后,陷入睡眠状态
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {    //睡醒后尝试获取锁b,锁b由线程2拥有
                    System.out.println("我获得了锁 a 和 b");
                }
            }
        }).start();


        new Thread(() -> {
            synchronized (b) {  //线程2得到锁b
                synchronized (a) {    //线程2尝试获取锁a,此时锁a由线程1拥有
                    System.out.println("我获得了锁 a 和 b");
                }
            }
        }).start();
    }
}

class A {
}

class B {
}

 通过jsp命令查到进程id

通过jstack和进程id得到进程的状况 

场景3:堆内存占用内存过大,排查流程?

Eg

package com._12306.SellTickets;

import java.util.concurrent.TimeUnit;

public class Test10 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");   //第一个阶段
        TimeUnit.SECONDS.sleep(30);
        byte[] bytes = new byte[1024 * 1024 * 10]; // 10MB
        System.out.println("2...");   //第二个阶段,内存加到10M
        TimeUnit.SECONDS.sleep(20);
        bytes = null;  //此处释放引用
        System.gc();  //垃圾回收
        System.out.println("3...");  //第三个阶段,垃圾回收后的状态
        TimeUnit.SECONDS.sleep(100);
    }

}

使用jconsole命令打开可视化窗口

场景4: 多次垃圾回收之后,堆内存占用依然很高。

使用jvisualvm命令打开可视化窗口,与上面jconsole不同的是,这里可以dump下堆内存的信息,查看到底是哪个对象占用的大量内存。

 java源代码

package com._12306.SellTickets;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Test11 {

    public static void main(String[] args) throws InterruptedException {
        List<Student> list = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            list.add(new Student());
        }
        TimeUnit.SECONDS.sleep(200000L);
    }

}

class Student{
    private byte[] bytes = new byte[1024*1024];
}

常量池中的字符串

String s1 = “a”,String s2 = “b”

String s3 = s1 + s2  与  String s4 = “ab” 是否相等

这道题困扰过很多初学者,包括博主在内。但凡事有因才有果,知道了结果,就会想去知道它的过程。现在我从字节码的角度来解释它的底层原理

Eg:

package com._12306.SellTickets;


//StringTable是一个
public class Test12 {

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

javap -v 反编译Test12.class

Classfile /Users/apple/IdeaProjects/aliossdemo/target/classes/com/_12306/SellTickets/Test12.class
  Last modified 2021-9-11; size 516 bytes
  MD5 checksum 58ed70d6cf6fa5fab5e81a28b5e092a2
  Compiled from "Test12.java"
public class com._12306.SellTickets.Test12
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = String             #25            // a
   #3 = String             #26            // b
   #4 = String             #27            // ab
   #5 = Class              #28            // com/_12306/SellTickets/Test12
   #6 = Class              #29            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/_12306/SellTickets/Test12;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               s1
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               s2
  #21 = Utf8               s3
  #22 = Utf8               SourceFile
  #23 = Utf8               Test12.java
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = Utf8               a
  #26 = Utf8               b
  #27 = Utf8               ab
  #28 = Utf8               com/_12306/SellTickets/Test12
  #29 = Utf8               java/lang/Object
{
  public com._12306.SellTickets.Test12();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/_12306/SellTickets/Test12;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 6
        line 9: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;
}
SourceFile: "Test12.java"

执行 String s1 = "a",分为三步

  1. 根据指令从运行常量池得到这个字面量
  2. 从StringTable中获取这个字面量对象,获取不到会将这个字面量封装成对象放入StringTable中
  3. 把变量s1的引用放入成员变量表中

Stringtable(串池)jdk1.8时存放在堆中(方法区中有展示),底层是hash表,大小是固定的,不能扩容。

Eg1:String s3 = s1 + s2  与  String s4 = “ab” 是否相等

package com._12306.SellTickets;

public class Test12 {

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        /**
         *         9: new           #5       // class java/lang/StringBuilder   新建一个StringBuilder对象
         *         12: dup
         *         13: invokespecial #6      // Method java/lang/StringBuilder."<init>":()V    初始化StringBuilder
         *         16: aload_1               //                                    加载局部变量变量s1(得到引用地址)
         *         17: invokevirtual #7      // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   做拼接操作 sb.append("a")
         *         20: aload_2               //                                    加载局部变量变量s2(得到引用地址)
         *         21: invokevirtual #7      // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   做拼接操作 sb.append("b")
         *
         * 
         *         24: invokevirtual #8      // Method java/lang/StringBuilder.toString:()Ljava/lang/String;    调用StringBuilder的toString()方法,在堆中创建一个新的String对象
         *         27: astore        4       //                                    回写变量s4至局部变量表
         */
        String s4 = s1 + s2;        //s4是堆中的对象,s3是StringTable中的对象
        System.out.println(s3 == s4);   //false
    }
}

不相等

String s3 = s1 + s2 通过StringBuilder做了两次变量拼接操作,最后在堆内存中创建了一个 “ab” 对象。

String s4 = “ab” 在常量池中创建了一个对象

两者的地址引用不同

Eg2:String s3 = “ab”  与  String s4 = “a“+”b” 是否相等

相等,java编译器编译过程中,知道这是两个常量,默认先做拼接操作,再去常量池寻找对象。

​​​​​​​较为简单,这里就不演示了

Eg3:使用Intern() 方法把堆中的String放入StringTable中

package com._12306.SellTickets;

public class Test13 {

    public static void main(String[] args) {

        /**
         *          0: new           #2                  // class java/lang/StringBuilder           创建StringBuilder对象
         *          3: dup
         *          4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V     初始化StringBuilder
         *          7: new           #4                  // class java/lang/String                  创建String对象
         *         10: dup
         *         11: ldc           #5                  // String a                                加载常量池中的字面量a,如果没有在串池中创建
         *         13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V          初始化String
         *         16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   拼接串池中的对象a
         *         19: ldc           #8                  // String b                                加载常量池中的字面量b,如果没有在串池中创建
         *         21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;   拼接串池中的对象b
         *         24: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;        将StringBuilder对象转换成String(堆中创建的)
         *         27: astore_1                                                                          将变量 s 加载至成员变量表
         */
        String s = new String("a") + "b";

        /**
         *         28: aload_1                      从成员变量表中加载变量 s
         *         29: invokevirtual #10                 // Method java/lang/String.intern:()Ljava/lang/String;   调用String中的intern()方法
         *         32: astore_2                     将变量 s2 加载至成员变量表
         */
        String s2 = s.intern();  //将这个字符串对象尝试放入串池,如果有则不会放入。如果没有则放入,返回串池中的对象

        System.out.println(s2 == s|| s == "ab");     //此时字符串对象ab 已放入常量池,从常量池取到的也是放进去的(就是说再拿"ab"字符串的时候不必根据地址引用从堆中拿了,而是从常量池中获取)

    }

}

例子中的变量s原本指的是堆内存中字符串对象“ab”,现在放入常量池中,再次拿取时变量s已经化身为常量池中的元素。

Eg4:String s = new String("ab")+"c" 创建了几个String对象

3个,StringTable中的 “ab”,“c”,堆中的“abc”;

有的人会说4个,第四个应该指的是StringTable(串池)中 “abc”这个。

我证明下为什么是3个

package com._12306.SellTickets;

public class Test12 {

    public static void main(String[] args) {

        /**
         *          0: new           #2                  // class java/lang/StringBuilder           堆中创建StringBuilder对象
         *          3: dup
         *          4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V     调用StringBuilder初始化方法
         *          7: new           #4                  // class java/lang/String                  创建String对象
         *         10: dup
         *         11: ldc           #5                  // String ab                               通过指令加载常量池中的 字符串对象ab,如果没有则创建  第一个 常量池中的 ab
         *         13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V     初始化String对象
         *         16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  调用sb的append()方法
         *         19: ldc           #8                  // String c                                加载常量池中的  字符串对象c,如果没有则创建, 第二个 常量池中的 c
         *         21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  做拼接操作,拼接后 abc
         *         24: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;   StringBuilder到String,堆中创建了新String对象 abc。第三个
         *         27: astore_1
         *         28: return
         */
        String s = new String("ab") + "c";  // s = new String("abc");

        String intern = s.intern();     //如果放不进去,说明串池中原来就有对象"abc",s == "abc"为false,那就说明我错了。如果放进去了,说明串池中此前并没有对象"abc",那就是产生了3个对象

        System.out.println(s == "abc");
    }
}

性能调优

场景1:从数据库读取表中某个varchar字段的所有数据并返回至页面。

package com._12306.SellTickets;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
 * -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Test17 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();  //模拟数据库读取字符串
        System.in.read();   //点击空格开始读取
        for (int i = 0; i < 10; i++) {  //读10次
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();     //记录开始时间
                while (true) {
                    line = reader.readLine();
                    if (line == null) {
                        break;
                    }
                    address.add(line);      //将读到的单词添加到list集合中(循环10次,有9次读到的数据都是重复的,会不断的在堆中创建String对象,消耗内存)
                }
                System.out.println("cost:" + (System.nanoTime() - start) / 1000000);    //计算时间
            }
        }
        System.in.read();


    }
}

按下空格,开始读取文件,因为对象有引用,不能被GC回收,内存很快就被吃满

修改代码,将字符串对象加入至串池中 

package cn.itcast.jvm.t1.stringtable;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示 intern 减少内存占用
 * -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
 * -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
 */
public class Demo1_25 {

    public static void main(String[] args) throws IOException {

        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());  //尝试将字符串加入StringTable中
                }
                System.out.println("cost:" +(System.nanoTime()-start)/1000000);
            }
        }
        System.in.read();


    }
}

内存使用50%

串池中的字符串对象是不可以重复的,其余没有加入串池的对象会被GC回收掉

场景2:串池大小对性能的影响

通过VM参数:-XX:StringTableSize=1009  设置串池的哈希桶的个数(最小为1009)

Eg

package com._12306.SellTickets;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 演示串池大小对性能的影响
 * -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
 */
public class Test16 {

    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
            String line = null;
            long start = System.nanoTime();
            while (true) {
                line = reader.readLine();
                if (line == null) {
                    break;
                }
                line.intern();
            }
            System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
        }


    }
}

修改VM参数设置 -XX:StringTableSize=200000  设置串池的哈希桶的个数为20万个

因为哈希桶的个数越少,哈希碰撞的概率就会越大,一个哈希桶就需要挂载多个节点,当有新的字符串对象加入时,会在哈希桶内一个个遍历,消耗时间。

哈希桶的个数越多,字符串对象就会很分散,通过哈希值可以轻易知道串池中有无该元素。

但是它耗内存。以空间换时间,用的时候还是需要权衡。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海上钢琴师_1900

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值