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",分为三步
- 根据指令从运行常量池得到这个字面量
- 从StringTable中获取这个字面量对象,获取不到会将这个字面量封装成对象放入StringTable中
- 把变量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万个
因为哈希桶的个数越少,哈希碰撞的概率就会越大,一个哈希桶就需要挂载多个节点,当有新的字符串对象加入时,会在哈希桶内一个个遍历,消耗时间。
哈希桶的个数越多,字符串对象就会很分散,通过哈希值可以轻易知道串池中有无该元素。
但是它耗内存。以空间换时间,用的时候还是需要权衡。