当前位置:网站首页>【JVM调优实战100例】04——方法区调优实战(上)
【JVM调优实战100例】04——方法区调优实战(上)
2022-07-06 17:10:00 【半旧518】
前 言
作者简介:半旧518,长跑型选手,立志坚持写10年博客,专注于java后端
专栏简介:实战案例驱动介绍JVM知识,教你用JVM排除故障、评估代码、优化性能
文章简介:介绍方法区概念、帮助你深入理解常量池、String table调优等
文章目录
7.方法区
7.1 定义
方法区是java虚拟机中所有线程共享的共享区域,主要存放类的结构相关信息(成员变量,方法、构造器的代码),运行时常量池,类加载器。方法区在虚拟机启动时被创建,在逻辑上属于堆的组成部分(具体产商实现时不一定遵守逻辑上的划分标准)。
在jdk1.8以前,方法区位于jvm的永久代,字符串存放在常量池。在jdk1.8以后,方法区则位于本地内存的元空间,不再占用JVM的内存空间,而字符串存在于堆。具体参考下图。
Tip:
方法区其实是逻辑上的概念,因为您可以发现,在jdk1.8以后,他甚至在物理存储空间上是拆分开的。
7.2 方法区内存溢出
通过下面代码可以演示方法区内存溢出。
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; 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); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
在jdk1.8以前的版本,将永久代设置为8m: -XX:MaxPermSize=10m
,在jdk1.8以后,将元空间设置为8m: -XX:MaxMetaspaceSize=8m
,将会报出OutOfMemoryError
.
在实际的工作场景中,spring、mybatis等框架都使用了cglib来动态生成class,因此框架使用不当是可能导致方法区内存溢出的。不过在jdk1.8以后方法区移到了元空间,空间充裕了很多,也由元空间进行垃圾回收,内存溢出的可能降低了。
7.3 常量池
下面是一个helloworld的代码。
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
计算机最终会把这段代码转换为二进制代码后执行,这段二进制代码包含类基本信息、类方法定义(包含指令)、常量池。我们切换到out路径下对应的class文件所在目录,使用反编译命令javap -v xxx.class
将二进制代码的内容转为可读的代码一探究竟。
Classfile /F:/资料 解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t5/helloworld.class
Last modified 2021年9月9日; size 567 bytes
SHA-256 checksum 37204bf6e654f64ae56660a1e8becfaa98b3ae7592b81b4b6e331de92a460b96
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // cn/itcast/jvm/t5/HelloWorld
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
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 // cn/itcast/jvm/t5/HelloWorld
#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 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.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 cn/itcast/jvm/t5/HelloWorld
#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 cn.itcast.jvm.t5.HelloWorld();
descriptor: ()V
flags: (0x0001) 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 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) 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: "HelloWorld.java"
如上,常量池原来就是一张常量表。注意到其中方法定义部分如#2
等内容,这其实就对应着常量池(表)Constant pool
的常量。
常量池是.class文件中的,当类被加载时,常量池的内容就会被放入运行时常量池,并且其中的符号地址将会变为真实地址。
7.4 String table
下面看一个面试题。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s3 == s4);
System.out.println(s3 == s4);
要回答这个问题,需要搞清楚string table,先从最简单的说起,反编译下列代码。
public class Demo1_22 {
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
}
}
截取部分。可以看到jvm从#2
取String a存放到了LocalVariableTable
的Slot1,其他串与此类似。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) 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 11: 0
line 12: 3
line 13: 6
line 21: 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;
在jvm启动时,常量池中的内容都会加载到运行时常量池中,但是此时a,b,ab都还只是一个符号,而不是字符串对象。只有当执行到具体的指令,如0: ldc #2
才会创建字符串对象"a"。于此同时,jvm还会去String table[]
中去找是否有"a"这个字符串,如果没有则将其加入String table[]
。注:String table[]其实是hashtable 结构,不能扩容。
在java代码中新增s4,并反编译。
String s4 = s1 + s2;
反编译结果如下。
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
以上操作等同于。
new StringBuilder().append("a").append("b").toString()
其中toString()
的方法实现方式是:new String("ab")
。所以s3 == s4的结果为fasle.
System.out.println(s3 == s4); //false
接着我们在代码中新增s5.
String s5 = "a" + "b";
反编译结果如下。
29: ldc #4 // String ab
31: astore 5
原来,javac编译时帮助我们进行了优化, 它认为“a”,“b”是常量,结果不可能会发生改变,于是结果直接在编译期确定为ab了。并且,由于"ab"在String table中已经存在,因此不会创建新的字符串对象了。
System.out.println(s3 == s4); //true
intern()方法可以把堆中的字符串对象放入串中,参考以下代码。
public class Demo1_23 {
// String table["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b"); // 堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
System.out.println( s2 == x); //true,s2与x都是串池中的对象
System.out.println( s == x ); //false,s是堆中的对象,与串池中的对象是不同的对象
}
}
下面这种情况x2可以成功加入串池,因此结果为true。
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";
System.out.println(x1 == x2); //true
不过jdk1.6中调用intern()方法,会将字符串尝试放入串池,如果有则不会放入,如果没有则会复制一份放入串池,因此,串池中的对象与堆中的对象并不是同一个对象。上面同样的代码再jdk1.6中x1 == x2
返回false。
串池的特点总结如下。
7.5 String table的位置
在jdk1.6,string table置于常量池,而常量池位于永久代的方法区中。永久代只有full gc触发时才会进行回收,这就导致string table的回收效率低。jdk1.7将string table移到了堆中。
7.6 String table的垃圾回收
参考以下代码配置参数并运行。
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
打印信息如下
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->875K(9728K), 0.0028226 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->512K(2560K)] 2923K->958K(9728K), 0.0039494 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2560K->512K(2560K)] 3006K->1006K(9728K), 0.0020900 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
...
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 26231 = 629544 bytes, avg 24.000
Number of literals : 26231 = 1548152 bytes, avg 59.020
Total footprint : = 2657800 bytes
Average bucket size : 0.437
Variance of bucket size : 0.418
Std. dev. of bucket size: 0.646
Maximum bucket size : 4
string table底层是hashtable,是采用数组加链表的形式存储数据,可以看到存储字符串的桶的数量为60013个,字符串的数量为26231个。我们实际上创建的字符串个数是10 0000个,为什么打印出来的数量不符合呢?根据打印信息,原来是因为触发了GC操作。
7.7 String table调优
string table的本质是hashtable,而hashtable的性能和桶的个数密切相关,对于string table进行调优其实就是要对hashtable的桶的个数进行调节。
/**
* 演示串池大小对性能的影响
* -XX:+PrintStringTableStatistics
*/
public class Demo1_24 {
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);
}
}
}
配置并运行上列代码,打印信息如下。
cost:439
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13697 = 328728 bytes, avg 24.000
Number of literals : 13697 = 609024 bytes, avg 44.464
Total footprint : = 1097840 bytes
Average bucket size : 0.684
Variance of bucket size : 0.684
Std. dev. of bucket size: 0.827
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 481494 = 11555856 bytes, avg 24.000
Number of literals : 481494 = 29750344 bytes, avg 61.788
Total footprint : = 41786304 bytes
Average bucket size : 8.023
Variance of bucket size : 8.084
Std. dev. of bucket size: 2.843
Maximum bucket size : 23
配置参数-XX:StringTableSize=200000
,打印信息如下。
cost:393
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13697 = 328728 bytes, avg 24.000
Number of literals : 13697 = 609024 bytes, avg 44.464
Total footprint : = 1097840 bytes
Average bucket size : 0.684
Variance of bucket size : 0.684
Std. dev. of bucket size: 0.827
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 200000 = 1600000 bytes, avg 8.000
Number of entries : 481494 = 11555856 bytes, avg 24.000
Number of literals : 481494 = 29750344 bytes, avg 61.788
Total footprint : = 42906200 bytes
Average bucket size : 2.407
Variance of bucket size : 2.420
Std. dev. of bucket size: 1.556
Maximum bucket size : 12
配置参数-XX:StringTableSize=1009
,打印信息如下。
cost:4870
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 16327 = 391848 bytes, avg 24.000
Number of literals : 16327 = 698456 bytes, avg 42.779
Total footprint : = 1250392 bytes
Average bucket size : 0.816
Variance of bucket size : 0.811
Std. dev. of bucket size: 0.901
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 1009 = 8072 bytes, avg 8.000
Number of entries : 482764 = 11586336 bytes, avg 24.000
Number of literals : 482764 = 29845512 bytes, avg 61.822
Total footprint : = 41439920 bytes
Average bucket size : 478.458
Variance of bucket size : 432.042
Std. dev. of bucket size: 20.786
Maximum bucket size : 547
可以发现,当桶的数量更多时,哈希冲突的可能性会减少,这样入池的时间会更少。
为什么要使用string table来存储字符串呢?因为这样可以节省空间,避免重复创建字符串对象。网络上流传twitter中存储用户信息包含地址项,如果不使用string table存储,需要约30G内存,但是这些地址可能包含大量重复地址,可能很多个用户都是来自于北京市中关村,于是twitter将地址信息入池,由string table创建存储,将这个内存空间降低至数十M。
通过下面实例来感受这一过程。
/**
* 演示 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);
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
在键盘键入前,没有读进行数据读取操作,键入后则进行了数据读取并存入了address中。在这两个时间节点采用jvisualvm中sampler进行取样。结果如下。
修改代码address.add()
address.add(line.intern());
边栏推荐
- stm32F407-------DAC数模转换
- New feature of Oracle 19C: automatic DML redirection of ADG, enhanced read-write separation -- ADG_ REDIRECT_ DML
- 在jupyter中实现实时协同是一种什么体验
- Alexnet experiment encounters: loss Nan, train ACC 0.100, test ACC 0.100
- Chapter II proxy and cookies of urllib Library
- Part 7: STM32 serial communication programming
- 腾讯云 WebShell 体验
- 深度学习框架TF安装
- Learn to use code to generate beautiful interface documents!!!
- Markov decision process
猜你喜欢
随机推荐
Article management system based on SSM framework
AI super clear repair resurfaces the light in Huang Jiaju's eyes, Lecun boss's "deep learning" course survival report, beautiful paintings only need one line of code, AI's latest paper | showmeai info
建立自己的网站(17)
Advanced learning of MySQL -- Fundamentals -- four characteristics of transactions
New feature of Oracle 19C: automatic DML redirection of ADG, enhanced read-write separation -- ADG_ REDIRECT_ DML
Value Function Approximation
随时随地查看远程试验数据与记录——IPEhub2与IPEmotion APP
Levels - UE5中的暴雨效果
Leetcode(547)——省份数量
Basic information of mujoco
一行代码实现地址信息解析
接口(接口相关含义,区别抽象类,接口回调)
Dell筆記本周期性閃屏故障
Advanced learning of MySQL -- basics -- basic operation of transactions
What kind of experience is it to realize real-time collaboration in jupyter
Tencent cloud webshell experience
.class文件的字节码结构
学习光线跟踪一样的自3D表征Ego3RT
Meet the level 3 requirements of ISO 2.0 with the level B construction standard of computer room | hybrid cloud infrastructure
ZYNQ移植uCOSIII