JVM学习笔记——>(小结)代码优化

1、逃逸分析

对未发生逃逸的对象实例进行栈上分配,在jdk7之后就会自动添加,之前版本可通过-XX:+DoEscapeAnalysis开启

个人理解:栈上分配的前提是开启逃逸分析,以及开启标量分配

-Xms256m -Xmx256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+PrintFlagsFinal

对这个的测试结果可以发现,自动开启了标量替换
在这里插入图片描述

1.1、栈上分配

将堆分配转化为栈分配。如果一个对象在子程序中被分配,要是指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

  • JIT编译器在编译器期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
  • 局部变量表只能用于存储八种数据类型、对象引用、以及returnAddress类型,而要是对象存储,既只能通过标量替换(本节第三点笔记)将其换成基本数据类型之后再放入局部变量表中
  • 常见的栈上分配的场景
    • 在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递
class User{

}
public class StackAllocation {

    public static void main(String[] args) {
        // 开始时间
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("总耗时:"+(end - start)+"ms");
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private static void alloc(){
        User user = new User();//未发生逃逸
    }

}

结果1(未开启逃逸分析):

-Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails

总耗时:62ms
Heap
 PSYoungGen      total 305664K, used 188744K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 72% used [0x00000000eab00000,0x00000000f6352068,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3734K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 411K, capacity 428K, committed 512K, reserved 1048576K

Process finished with exit code 130

打开jvisualvm可以观察到未打开逃逸分析的是将所有未发生逃逸的user实例对象放入堆内存当中(消耗了72%的Eden区)

在这里插入图片描述

结果2(开启了逃逸分析):

-Xms1G -Xmx1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails

总耗时:4ms
Heap
 PSYoungGen      total 305664K, used 31457K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 12% used [0x00000000eab00000,0x00000000ec9b8588,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3734K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 411K, capacity 428K, committed 512K, reserved 1048576K

Process finished with exit code 130

打开jvisualvm可以观察到打开逃逸分析的是将部分未发生逃逸的user实例对象放入堆内存当中(消耗了12%的Eden区),其余放入栈中

在这里插入图片描述
额外:
还有一种就是将内存改小一点(例如256M),会发现未开启逃逸分析情况下会触发 GC,而开启逃逸分析情况下则不会触发GC。

1.2、同步策略

如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

  • 线程同步的代价是相当高的,同步的后果是降低并发性和性能
  • 在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫做同步省略,也叫做锁消除。

1.3、分离对象或标量替换

通过-XX:+EliminateAllocations:开启了标量替换(默认开打),允许将你的对象打散分配在栈上。

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或者全部)可以步存储在内存,而是存储在cpy寄存器中。

标量是指一个无法再分解成更小数据的数据。Java中的原始数据类型就是标量。
相对的,还可以再分的叫做聚合量,Java中的对象就是聚合量,因此对象可以分为其他标量和聚合量。

代码说明

public class ScalarReplace {
    private static class User{
        private int id;
        private String name;
    }
    public static void alloc(){
        User user  = new User();//未发生逃逸
    }
    public static void main(String[] args) {
        // 开始时间
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("总耗时:"+(end - start)+"ms");
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果1(未开启标量替换):

-Xms100m -Xmx100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:-EliminateAllocations

[GC (Allocation Failure) [PSYoungGen: 25600K->728K(29696K)] 25600K->736K(98304K), 0.0009363 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 26328K->776K(29696K)] 26336K->792K(98304K), 0.0007085 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 26376K->744K(29696K)] 26392K->760K(98304K), 0.0006228 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 26344K->712K(29696K)] 26360K->728K(98304K), 0.0005941 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 26312K->712K(29696K)] 26328K->728K(98304K), 0.0006569 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 26312K->728K(32768K)] 26328K->744K(101376K), 0.0005934 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 32472K->0K(32768K)] 32488K->664K(101376K), 0.0008520 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 31744K->0K(32768K)] 32408K->664K(101376K), 0.0002507 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
总耗时:49ms
Heap
 PSYoungGen      total 32768K, used 23178K [0x00000000fdf00000, 0x0000000100000000, 0x0000000100000000)
  eden space 31744K, 73% used [0x00000000fdf00000,0x00000000ff5a28b0,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 68608K, used 664K [0x00000000f9c00000, 0x00000000fdf00000, 0x00000000fdf00000)
  object space 68608K, 0% used [0x00000000f9c00000,0x00000000f9ca6080,0x00000000fdf00000)
 Metaspace       used 3748K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 411K, capacity 428K, committed 512K, reserved 1048576K

结果2(开启标量替换):

-Xms100m -Xmx100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations

总耗时:5ms
Heap
 PSYoungGen      total 29696K, used 8221K [0x00000000fdf00000, 0x0000000100000000, 0x0000000100000000)
  eden space 25600K, 32% used [0x00000000fdf00000,0x00000000fe7074e0,0x00000000ff800000)
  from space 4096K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x0000000100000000)
  to   space 4096K, 0% used [0x00000000ff800000,0x00000000ff800000,0x00000000ffc00000)
 ParOldGen       total 68608K, used 0K [0x00000000f9c00000, 0x00000000fdf00000, 0x00000000fdf00000)
  object space 68608K, 0% used [0x00000000f9c00000,0x00000000f9c00000,0x00000000fdf00000)
 Metaspace       used 3744K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 411K, capacity 428K, committed 512K, reserved 1048576K

Process finished with exit code 130

结论:

  • 标量替换开启之后,可以发现代码运行并没有产生GC,以及运行时间大大缩减。
  • 目前很多书籍都是基于jdk7以前的版本,jdk已经发生了很大的变化,intern字符串的缓存和静态变量曾经都被分配在永久代,而jdk8及之后的永久代都被元空间所替代,但是intern字符串的缓存和静态变量并不是转移到了元空间区,而是直接分配到了堆上,所以这一点同样符合前一节的结论:对象实例都是分配在堆上的
  • 意思就是说,方法内如果这是一个未发生逃逸的对象,则会将其进行标量替换(将对象分配成基本数据类型),然后将分配好的数据放入虚拟机栈的局部变量表中,这一步又是栈上分配,但整个过程来说,这并不是对象,相对的来说,如果这是个对象,他仍然是方法堆中,所以这个拆分的过程不影响上一节的结论,既“对象实例都是分配在堆上的”,这句话是对的。
  • 所谓的栈上分配,其实就是分配到局部变量表中,而局部变量表中只能存放基本类型的变量和对象引用,所以必须通过标量替换将对象打散成基本类型变量,这前提是又要依赖逃逸分析,即对象未发生逃逸
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老哥不老

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

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

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

打赏作者

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

抵扣说明:

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

余额充值