Java-基础-05-JVM-6- 调优

74 阅读10分钟

1 堆空间如何设置

在分代模型中,各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。

活跃数据的大小:应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是FullGC后堆中老年代占用空间的大小。 可以通过GC日志中FullGC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。、

image.png 例如,根据GC日志获得老年代的活跃数据大小为 300 M,那么各分区大小可以设为:

总堆: 1200 MB= 300 MB× 4

新生代: 450 MB= 300 MB× 1. 5

老年代: 750 MB= 1200 MB- 450 MB

2 扩容新生代能提高 GC 效率吗?

通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁MinorGC,因此可以通过增大新生代空间来降低MinorGC的频率。例如在相同的内存分配率的前提下,新生代中的Eden区增加一倍,MinorGC的次数就会减少一半。扩容Eden区虽然可以减少MinorGC的次数,但会增加单次MinorGC时间啊,单次时间增加了,是不是也白忙活了!!!单次MinorGC时间由以下两部分组成:T 1 (扫描新生代)和T 2 (复制存活对象到Survivor区)如下图:

image.png

扩容前:新生代容量为R,假设对象A的存活时间为 750 ms,MinorGC间隔 500 ms,那么本次MinorGC时间=T 1 (扫描新生代R)+T 2 (复制对象A到S)。

扩容后:新生代容量为 2 R ,对象A的生命周期为 750 ms,那么MinorGC间隔增加为 1000 ms,此时MinorGC对象A已不再存活,不需要把它复制到Survivor区,那么本次GC时间 = 2 ×T 1 (扫描新生代R),没有T 2 复制时间。可见,扩容后,MinorGC时增加了T 1 (扫描时间),但省去T 2 (复制对象)的时间,更重要的是对于虚拟机来说,复制对象的成本要远高于扫描成本,所以,单次 MinorGC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。所以当 JVM 服务中存在大量短期临时对象,扩容新生代空间后,MinorGC频率降低,对象在新生代得到充分回收,只有生命周期长的对象才进入老年代。这样老年代增速变慢,MajorGC频率自然也会降低。但是如果堆中短期对象很多,那么扩容新生代,单次MinorGC时间不会显著增加。

总结的经验就是:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。

3 JVM 是如何避免 Minor GC 时扫描全堆的?

新生代GC和老年代的GC是各自分开独立进行的。

新生代对象持有老年代中对象的引用,老年代也可能持有新生代对象引用,这种情况称为“跨代引用”。

因它的存在,所以MinorGC时也必须扫描老年代。JVM是如何避免MinorGC时扫描全堆的?经过统计信息显示,老年代持有新生代对象引用的情况不足 1 %,根据这一特性JVM引入了卡表(cardtable)来实现这一目的。

image.png

卡表的具体策略是将老年代的空间分成大小为 512 B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表 3 被标记为脏,之后MinorGC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。

4 常量池

4.1 Class 常量池 ( 静态常量池 )

在class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 (ConstantPoolTable),用于存放编译期间生成的各种字 面量和符号引用。

image.png

字面量:给基本类型变量赋值的方式就叫做字面量或者字面值。

比如:Stringa=“b”,这里“b”就是字符串字面量,同样类推还有整数字面值、浮点类型字面量、字符字面量。

符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA在编译的时候一个每个java类都会被编译成一个class 文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续JVM类加载会具体讲到)就是为了 把这个符号引用转化成为真正的地址的阶段。 一个java类(假设为People类)被编译成一个class文件时,如果People类引用了Tool类,但是在编译时People类并不知道引用类的实际内存地址, 因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载People类时,此时可以通过虚拟机获取Tool类的实际内存地址,因此便可以既将 符号org.simple.Tool替换为Tool类的实际内存地址。

4.2 运行时常量池

运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:

从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。(这个是虚拟机规范中的描述,很生涩)运行时常量池是在类加载完成之后,将Class常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。

运行时常量池在JDK 1. 7 版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

在JDK 1. 8 中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will alwaysbeyourfather"。变动的只是方法区中内容的物理存放位置,但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的。

4.3 字符串常量池

字符串常量池这个概念是最有争议的,King老师翻阅了虚拟机规范等很多正式文档,发现没有这个概念的官方定义,所以与运行时常量池的关系不去抬杠,我们从它的作用和JVM设计它用于解决什么问题的点来分析它。以JDK 1. 8 为例,字符串常量池是存放在堆中,并且与java.lang.String类有很大关系。设计这块内存区域的原因在于:String 对象作为 Java语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。

所以要彻底弄懂,我们的重心其实在于深入理解String

5 String

String 类分析( JDK 1. 8 )

String对象是对 char数组进行了封装实现的对象,主要有 2 个成员变量:char数组,hash值。

image.png String 对象的不可变性

了解了 String对象的实现后,你有没有发现在实现代码中 String类被final关键字修饰了,而且变量 char数组也被final修饰了。 我们知道类被final修饰代表该类不可继承,而char[]被 final+private 修饰,代表了 String对象不可被更改。Java实现的这个特性叫作 String 对象的 不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。 Java 这样做的好处在哪里呢?

第一,保证 String 对象的安全性。假设String 对象是可变的,那么String 对象将可能被恶意修改。

第二,保证 hash属性值不会频繁变更,确保了唯一性,使得类似 HashMap容器才能实现相应的 key-value 缓存功能。

第三,可以实现字符串常量池。在 Java中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 Stringstr=“abc”;另一种是字符串变量通过 new形式的创建,如 Stringstr=newString(“abc”)。

String 的创建方式及内存分配的方式

1Stringstr=abc ”; 当代码中使用这种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中 被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。(str只是一个引用)

image.png 2Stringstr=newString(abc) 首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new时,JVM命令将会调用

String的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String对象;最后,str将引用 String对象。

image.png 4 、使用new,对象会创建在堆中,同时赋值的话,会在常量池中创建一个字符串对象,同时这个堆中对象的成员变量会引用了常量池中的字符串对象。 存在引用关系。 publicclassLocation{ privateStringcity;

privateStringregion; }

image.png

image.png

4Stringstr 2 ="ab"+"cd"+"ef"; 编程过程中,字符串的拼接很常见。前面我讲过 String对象是不可变的,如果我们使用String 对象相加,拼接我们想要的字符串,是不是就会产生多 个对象呢?例如以下代码: 分析代码可知:首先会生成 ab对象,再生成 abcd对象,最后生成 abcdef对象,从理论上来说,这段代码是低效的。 编译器自动优化了这行代码,编译后的代码,你会发现编译器自动优化了这行代码,如下 Stringstr="abcdef";

5 、大循环使用+

image.png

intern

String的 intern方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。

image.png 1 、newSting() 会在堆内存中创建一个a的String对象,king"将会在常量池中创建 2 、在调用intern方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。 3 、调用newSting() 会在堆内存中创建一个b的String对象。

4 、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。 所以 a和 b引用的是同一个对象。

如果需要查看String的编译优化,需要使用到反编译工具,推荐JD-GUI:java-decompiler.github.io/