吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1446|回复: 28
收起左侧

[学习记录] jvm基础知识学习,感兴趣的可以看下

  [复制链接]
chengxuyuan01 发表于 2022-8-4 15:12
本帖最后由 chengxuyuan01 于 2022-8-4 15:15 编辑

本来准备只花三个星期对照教学视频整理好的,最后多花了两个星期,看成果啦:lol

JVM入门

jvm基础

什么是jvm

定义:Java Virtual Machine -java 程序的运行环境(java 二进制字节码的运行环境)

好处:

  • 一次编写,到处运行的基石
  • 自动内存管理机制,垃圾回收
  • 数组下标越界检查
  • 多态

比较:

jvm,jre,jdk

用处

  • 用户
  • 理解底层实现原理
  • 中高级程序员的必备技能

常见的jvm

内存结构

程序计数器 PC Register

定义

Program Counter Register 程序计数器(寄存器)

  • 在程序执行过程中,记录吓一跳jvm指令的执行地址
    • 在代码运行过程中,java代码会被编译成有序的jvm指令,再由解释器解释成程序能够识别的二进制指令
    • 在上一条指令执行完成后,程序会从程序计数器中获取下一条指令的地址
  • 特点:
    • 线程私有
    • 不会存在内存溢出的区

虚拟机栈

  • 先进后出结构

  • 线程运行时需要的内存空间(线程私有)

  • 虚拟机栈由多个栈帧组成(每个方法运行时需要的内存-》参数、局部变量、返回地址)

  • 每个线程只能有一个活动栈帧,对应着正在执行的那个方法

 public static void main(String[] args) {
        method1();
    }

    private static void method1() {

        method2(1,2);
    }

    private static int method2(int a ,int b) {

        int c = a + b;

        return c;
    }

备注:debugger运行,可通过Frames变化查看当前线程对应虚拟机栈的内存变化,入栈操作为main-》method1-》method2,实际的执行顺序为反序

问题:

  1. 垃圾回收是否涉及栈内存?

    不会:栈内存在每次方法执行结束后都会被出栈,释放内存,所以不需要垃圾回收

  2. 栈内存分配越大越好吗?

    栈内存的默认大小都是1024kb(mac,linux..)

    在windows环境下的默认大小是根据内存大小进行分配的

    在相同内存大小下,如果栈分配的内存大小不一致,栈内存大的程序所能调用的线程数会比栈内存小的程序少

  3. 方法内的局部变量是否线程安全?

    线程安全,方法内的局部变量对每个线程都会分配一份单独的内存空间,每个栈内的内存空间互不影响,如果变量是共享的,会存在线程安全问题。

    变量是否线程安全:

    • 是否是方法内的局部变量
    • 局部变量是否逃离方法的作用范围(参数或返回)
    • 如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全

栈内存溢出

  • 栈内栈帧过多(方法的递归调用)
  • 栈帧过大(一个栈帧大小就超过了栈内存大小)
private static int count;

public static void main(String[] args) {

        try {
            method3();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            System.out.println(count);
        }

    }

private static void method3() throws Exception {
        count++;
        method3();
    }

默认栈内存大小下,递归发生了23522次

修改VM option栈内存大小参数 Xss256k 后:-递归次数变为3558

java.lang.StackOverflowError

线程运行诊断

案例1:cpu占用过多

定位:

top命令查看当前系统中进程占用资源情况

ps H(线程中的进程数) -eo(显示的字段) pid,tid,%cpu -展示进程中线程的属性展示

jstack 进程id 查看进程中的线程信息

  • 根据进程id找到有问题的线程,进一步定位到问题代码的源代码行数

案例2:程序运行很长时间没有结果

jstack 进程id 查看进程中的线程信息

  • 找到线程死锁代码

本地方法栈-Native method stack

给本地方法的运用提供内存空间(java代码存在局限,无法直接和操作系统进行交互,需要通过C等代码编写的native方法和操作系统形成交互)

堆-Heap

通过new关键字,创建对象都会使用堆内存

特点:

  • 线程共享,堆中对象都需要考虑线程安全的问题
  • 有垃圾回收机制

堆内存溢出

堆内的对象都存在引用,且一直存在对象的新增

public static void main(String[] args) {

        int i = 0;

        try {
            List<String> list = new ArrayList<>();

            String a = "hello";
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }

    }

Java.lang.OutOfMemoryError: Java heap space

-Xmx8m:设置堆内存大小

堆内存诊断

  1. jps工具

    • 查看当前系统中有那些java进程
    60064 RemoteJdbcServer
    7424 RemoteMavenServer
    10728
    1544 Test3
    33400 ApiApplication
    53148 Launcher
    6940 Jps
  2. jmap工具

    • 查看堆内存占用情况:jmap -heap 进程id
    ##堆配置
    Heap Configuration:
      MinHeapFreeRatio         = 0
      MaxHeapFreeRatio         = 100
      MaxHeapSize              = 4261412864 (4064.0MB)#最大内存配置
      NewSize                  = 88604672 (84.5MB)#新生代内存
      MaxNewSize               = 1420296192 (1354.5MB)#最大新生代内存
      OldSize                  = 177733632 (169.5MB)#老年代内存
      NewRatio                 = 2
      SurvivorRatio            = 8
      MetaspaceSize            = 21807104 (20.796875MB)
      CompressedClassSpaceSize = 1073741824 (1024.0MB)
      MaxMetaspaceSize         = 17592186044415 MB #元空间大小
      G1HeapRegionSize         = 0 (0.0MB)
    
    #伊甸区内存使用情况
    Eden Space:
      capacity = 66584576 (63.5MB)
      used     = 1331712 (1.27001953125MB)
      free     = 65252864 (62.22998046875MB)
      2.0000307578740157% used
    
  3. jconsole工具

    • 图形界面的,多功能的监测工具,可以连续监测:jconsole

    案例:垃圾回收后,内存依旧占用很高

    jvisualvm 可视化工具工具

方法区

  • 所有java虚拟机进程共享的区域
  • 存储类结构相关信息:成员变量、方法数据、成员方法、构造方法、特殊方法
  • 运行时常量池
  • 在虚拟机启动时被创建
  • 逻辑上是堆的存储部分

方法区内存溢出

hotspot 在jdk1.8之前在堆空间中划分永久代保存方法区内的数据

1.8以后,永久代被替代为元空间,且使用系统物理内存作为数据存储

一般不会出现内存溢出问题,需要修改相关jvm参数

-XX:MaxMetaspaceSize=8m:修改元空间最大内存为8m

 public static void main(String[] args) {
        int j = 0;
        try {
           Test4 test4 = new Test4();
            for (int i = 0; i < 10000; i++,j++) {
                //ClassWriter作用是生成类的二进制字节码
                ClassWriter wa = new ClassWriter(0);
                //版本号,public,类名,包名,父类,接口
                wa.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
                //返回byte
                byte[] code = wa.toByteArray();
                //执行了类的加载
                test4.defineClass("Class"+i,code,0,code.length);
            }

        }  finally {
            System.out.println(j);
        }
    }

结果:

Error occurred during initialization of VM
MaxMetaspaceSize is too small.

场景:

  • spring
  • mybatis

运行时常量池

  1. 静态常量池:.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
  2. 运行时常量池:则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池,除此外,运行期间也可以将新的常量放入池中,如String类的intern()方法,会在常量池查找是否存在一份equal相等的字符串,如果有则返回该字符串的引用,否则自己添加字符串进入常量池。

优点:

  • 实现了对象的共享,避免了频繁的创建和销毁对象而影响系统性能
  • 节省内存空间,如字符串常量池
    • 合并相同字符串只占用一个空间
    • 节省运行时间,进行比较时==比equals快,只判断引用是否相等就可以判断实际值是否相等

                  String s1 = "Hello";
         String s2 = "Hello";
         String s3 = "Hel" + "lo";
         String s4 = "Hel" + new String("lo");
         String s5 = new String("Hello");
         String s6 = s5.intern();
         String s7 = "H";
         String s8 = "ello";
         String s9 = s7 + s8;

         System.out.println(s1 == s2);  // true
         System.out.println(s1 == s3);  // true
         System.out.println(s1 == s4);  // false
         System.out.println(s1 == s9);  // false
         System.out.println(s4 == s5);  // false
         System.out.println(s1 == s6);  // true

s1 == s1 作为直接赋值,使用的字符串字面量,在编译时会直接加载到class文件的常量池中,内容被合并且指向一致

s1==s3 s3对象为拼接对象,但是作为拼接的两个字符串也是字面量,在编译时会被优化为s1 = "Hello",等同于s1和s2

s1==s4 s4中存在new的新对象,其地址为堆中地址,与常量池地址不一致,需要在等到运行时才会知道地址

s1==s9 s9为拼接对象,虽然s7,s8中使用的是字符串字面量,但是拼接s9时,s7,s8作为两个变量,地址不可预料,不能在编译时确定,所有不存在优化

s4==s5 两个都是new出来的堆对象,地址不可能一致

s1==s6 s6使用intern()方法在常量池中寻找,如果有直接调用常量池中的地址

特例:static修饰符

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
     String s = A + B;  // 将两个常量用+连接对s进行初始化 
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 } 

结果是一致对象。

原因:static修饰方法在编译时直接定死了AB的值和地址,所以s值也为定值

特例2:静态代码块

public static final String A; // 常量A
public static final String B;    // 常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
    // 将两个常量用+连接对s进行初始化   
     String s = A + B;   
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它们是同一个对象");   
     } else {   
         System.out.println("s不等于t,它们不是同一个对象");   
     }   
 } 

并不是同一个对象

原因:AB为变量,但是没有直接赋值,程序不知道它们何时赋值和赋予什么样的值,那么s值就不能作为一个定时被初始化,只能在运行时创建。

注意:

  • 运行时常量池中的常量基本来源于class文件中的常量池
  • 程序运行时,除非手动向常量池中添加常量(intern方法),否则jvm不会自动添加常量到常量池中

基本数据类型和其包装类中除了Float和Double外都实现了常量池技术,但是数值类型的常量池不能手动添加常量,程序启动时常量池中的常量就已经确定了,

如整形常量池中的常量范围:-128-127,(Byte,Short,Integer,Long,Character,Boolean)这五种包装类默认创建了数值【-128,127】之间的响应类型缓存数据,超过此范围后都会创建新的对象到堆中。

StringTable 串池

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池机制,避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(jdk1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    • 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
    • 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

位置:

1.6时,是跟随常量池放在永久代空间中

1.7,1.8后在堆中单独开辟一个空间存放StringTable

原因:

  • 永久代的回收效率不高

直接内存-Direct Memory

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本高,但读写性能高
  • 不受jvm内存回收管理

相较于原始的io操作,原始io在进行文件读写时需要先由操作系统划分内存空间,再由java程序划分内存空间,进行内容的复制,最后交由内核进行内容写入

直接内存同样也是系统开辟内存空间,但是这块内存空间java程序可以直接访问,减少了java自己开辟空间以及进行内容拷贝的时间

内存溢出

static int defaultMember = 1024*1024*100;
    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();

        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(defaultMember);
                list.add(byteBuffer);
                i ++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            System.out.println(i);
        }

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

-XX:+DisableExplicitGC:禁用显示的垃圾回收

System.gc:显示的垃圾回收,FUll GC,导致程序出现较长时间的停滞

垃圾回收

-XX:+PrintGCDetails -verbose:gc:打印垃圾回收详细参数

如何判断对象可以回收

引用计数法

弊端:如果存在相互引用的两个对象,会造成这两个对象一直无法被垃圾回收,它们的引用计数最少为1

可达性分析算法

jvm虚拟机底层分析算法

  • java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用连接找到该对象,找不到,表示可以回收
  • 哪些对象可以作为GC Root?
    • 系统类
    • native 类
    • 正在加锁的对象
    • 活动线程运行过程中,局部变量引用的对象

四种引用

  • 强引用

    • new() 方法生成对象赋值给变量
    • 在强引用未结束前,不会被垃圾回收
  • 软引用

    • 没有其它强引用对象引用它(只有软引用的对象引用了这个对象)

    • 发生垃圾回收,且内存不足时,会被回收

    • 可以配合引用队列来释放软引用本身

    /**
    * home.php?mod=space&uid=686208 ysx
    * home.php?mod=space&uid=341152 Test7
    * @description
    * @packagesName com.ysx.check
    * @createTime 2022/7/8 13:53
    **/
    /**-Xmx8m**/
    /**-XX:+PrintGCDetails -verbose:gc**/
    public class Test7 {
    
      private static int size = 1024 * 1024 * 2;
      public static void main(String[] args) {
    
    //        method1();
          method2();
    
      }
      //强引用导致内存溢出 Java.lang.OutOfMemoryError: Java heap space
      private static void method1() {
          List<byte[]> list = new ArrayList<>();
    
          for (int i = 0; i < 5; i++) {
              list.add(new byte[size]);
    
          }
      }
      //弱引用不会导致内存溢出
      private static void method2() {
          List<SoftReference<byte[]>> list = new ArrayList<>();
    
          for (int i = 0; i < 4; i++) {
              SoftReference<byte[]> ref = new SoftReference<>(new byte[size]);
    
              System.out.println(ref.get());
              list.add(ref);
              System.out.println(list.size());
          }
    
          System.out.println("循环结束:"+list.size());
    
          for (SoftReference<byte[]> ref : list) {
              System.out.println(ref.get());
          }
    
      }
    
      //使用引用队列,对软引用本身进行垃圾回收
      private static void method3() {
    
          //引用队列
          ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    
          List<SoftReference<byte[]>> list = new ArrayList<>();
    
          for (int i = 0; i < 4; i++) {
              //关联了软引用队列,当软引用所关联的byte[]被回收时,软引用自己会加入到queue中去
              SoftReference<byte[]> ref = new SoftReference<>(new byte[size],queue);
    
              System.out.println(ref.get());
              list.add(ref);
              System.out.println(list.size());
          }
    
          System.out.println("循环结束:"+list.size());
          //从引用队列中获取软引用对象,最先放入的最先取出
          Reference<? extends byte[]> poll = queue.poll();
    
          //如果队列中不为空,则该软引用对象所关联的对象被回收了,从集合中对该引用对象进行删除,并进行下一次流程
          while (poll != null) {
              list.remove(poll);
              poll = queue.poll();
          }
    
          System.out.println("===============================");
          for (SoftReference<byte[]> ref : list) {
              System.out.println(ref.get());
          }
      }
    
    }
  • 弱引用

    • 没有其它强引用对象引用它(只有弱引用的对象引用了该对象)

    • 只要发生垃圾回收,不管内存够不够,都会回收

    • 可以配合引用队列来释放软引用本身

    /**-Xmx20m**/
    /**-XX:+PrintGCDetails -verbose:gc**/
    public class Test8 {
    
      private static int size = 1024 * 1024 * 4;
      public static void main(String[] args) {
    
          method1();
    
      }
    
      //弱引用实例
      private static void method1() {
    
          List<WeakReference<byte[]>> list = new ArrayList<>();
    
          for (int i = 0; i < 10; i++) {
              WeakReference<byte[]> ref = new WeakReference<>(new byte[size]);
              list.add(ref);
              for (WeakReference<byte[]> w : list) {
                  System.out.println(w.get()+" ");
              }
              System.out.println();
          }
          System.out.println("循环结束:"+list.size());
    
      }
    }
  • 虚引用

    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放内存
  • 终结器引用

    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象

垃圾回收算法

标记清除 Mark Sweep

具体查看流程图:标记-清除

优点:速度快

缺点:清理出的内存不连续,出现大量内存碎片

标记整理 Mark Compact

具体查看流程图:标记-整理

在标记清除的基础上,将内存碎片向某个方向移动,进行整理,避免了内存碎片的出现

优点:不会产生内存碎片

缺点:存在对象的移动,对性能损耗较大

复制 Copy

具体查看流程图:复制

复制存活的对象到另一个区域,原区域内的对象全部清除,且进行复制区域和原区域的替换

优点:不会产生内存碎片

缺点:复制的空间也是从堆空间中划分的,相当于愿有的堆空间被划分为两块,程序只能同时对一块区域进行操作,且存在对象的移动,性能损耗大

分代垃圾回收

具体查看流程图:分代垃圾回收

堆内存

  • 新生代(生命周期较短的对象):清理频率高

    • 伊甸区
    • 幸存区From
    • 幸存区To
  • 老年代(生命周期较长对象):清理频率低,速度慢

当伊甸区内存达到上限后,会触发新生代垃圾回收(Minor GC - 可达性分析标记可回收对象),执行复制算法,复制幸存对象到幸存区To,且复制的对象的存活寿命 +1,并进行幸存区To和幸存区From的交换,继续生成对象,达到上限,进行第二次垃圾回收,便利幸存区From中是否有存活的对象,有就移动到幸存区To中,且寿命 +1,继续复制伊甸区中的存活对象到幸存区To,寿命 +1,再进行空间的替换,当幸存区中的对象寿命达到某个阈值(最大寿命是15,4bit),会将该对象晋升到老年代空间中,当新生代和老年代的空间都不足时,会触发Full GC进行整个空间的清理

当Minor GC触发时,会引发一次 stop the world,暂停其它用户的线程,等垃圾回收线程结束后,其它用户线程会恢复运行

当老年代空间不足时会先尝试触发Minor GC,如果空间还是不足,紧接着会触发Full GC,暂停其它用户的线程(算法不同,停顿时间较长),等垃圾回收完成后,其它用户线程恢复运行

大对象直接晋升到老年代

如果新生代中创建的对象大小超过新生代内存大小,不会触发Minor GC,该对象会直接晋升到老年代,如果大对象过多,会导致内存溢出

一个线程内的内存溢出不会导致整个Java程序的服务停止

jvm 参数

含有 参数
堆初始大小 -Xms
堆最大大小 -Xmx或-XX:MaxHeapSize=size
新生代大小 -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio和-XX:UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC前MinorGC -XX:+ScavengeBeforeFullGC

默认新生代内存分配比例:伊甸区:From:To=8:1:1

垃圾回收器

-XX:+UseSerialGC:设置垃圾回收器

  • 串行的垃圾回收
    • 单线程
    • 堆内存较小,单核cpu,适合个人电脑
  • 吞吐量优先
    • 多线程
    • 堆内存较大,多核cpu
    • 让单位时间内,STW(stop the world)的时间最短
  • 响应时间优先
    • 多线程,多核cpu
    • 堆内存较大,多核cpu
    • 尽可能让单次STW的时间最短

串行

-XX:+UseSerialGC=Serial + SerialOld:开启串行垃圾回收器(新生代复制算法,老年代标记整理算法)

吞吐量优先

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC:开启吞吐量优先的垃圾回收器

-XX:+UseAdaptiveSizePolicy:采用自适应大小调整策略

-XX:GCTimeRatio=radio:调整垃圾回收时间与总时间占比-计算公式:1/(1+radio)

-xx:MaxGCPauseMillis=ms:最大暂停毫秒数(默认200),此项与上一项目标对立

-XX:ParallelGCThreads=n:控制垃圾回收线程数

响应时间优先

-XX:UseConCMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld :老年代基于标记-清除算法的垃圾回收,且在垃圾回收阶段,其余用户线程可以可继续执行,存在初始标记和重新标记两次STW操作

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads:垃圾回收线程数  可用于计算的垃圾回收线程数

-XX:CMSInitiatingOccupancyFraction=percent:执行CMS垃圾回收的内存占比,当老年带的内存占比达到percent后进行垃圾回收,剩余内存需要保存其余线程在垃圾回收线程清理过程中生成的新垃圾

-XX:+CMSScavengeBeforeRemark:在重新标记之前对新生代进行一次垃圾回收UseParNewGC

响应时间优先的垃圾回收会造成大量的内存碎片,最终可能会导致垃圾回收的并发失败,此时垃圾回收器会退化为串行的单线程的垃圾回收器,进行一次内存的标记整理,此时消耗的时间较长

G1

定义:Garbage First

jdk1.7官方正式支持

jdk1.9官方默认垃圾回收器(取代cms垃圾回收)

适用场景:

  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大堆内地,会将堆划分未多个大小相等的Region(每个Region都可以作为单独的新生代,老年代)
  • 整体上是标记-整理算法,两个区域之间是复制算法

相关参数:

-XX:+UseG1GC

-XX:G1HeapRegionSize=size:设置区域大小

-XX:MaxGcPauseMillis=time:设置默认暂停目标

G1优化

> 字符串去重

  • 优点:节省大量内存
  • 略微多占用了cpu时间,新生代回收时间略微增加

-XX:+UseStringDeduplication

 String s1 = new String("hello");//char[]{'h','e','l','l','o'}
 String s2 = new String("hello");//char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果她们值一样,会让它们引用同一个char[]
  • 注意,与String.intern不一样
    • String.intern关注的是字符串对象
    • 而字符串去重关注的是char[]
    • 在jvm内部,使用了不同德字符串表

> 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不在使用,则卸载它所加载的所有类

-XX:+ClassUnloadingWithConcurrentMark 默认启用

> 回收巨型对象

  • 一个对象大于Region的一半时,称之为巨型对象
  • G1不会堆巨型对象进行拷贝
  • 回收时呗优先考虑
  • G1会跟踪老年代所有incomming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

> 并发标记时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为FullGC
  • jdk1.9之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • jdk1.9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空挡空间

垃圾回收调优

预备知识

  • 掌握GC相关的参数,会基本的空间调整

  • 掌握相关工具

  • 调优跟应用、环境有关,没有放之四海而皆准的法则

  • 内存

  • 锁竞争

  • cpu占用

  • io

确定目标

低延迟还是高吞吐量,选择合适的回收器

CMS,G1,ZGC

ParallelGC

最快的GC是不发送GC

查看Full GC前后的内存占用,考虑下面几个问题

  • 数据是不是太多
  • 数据表示是否太臃肿
    • 对象图
    • 对象大小
  • 是否存在内存泄漏

新生代调优

新生代特点

  • 所有的new操作的内存分配非常廉价
    • TABLE thread-local allocation buffer:线程私有内存分配
  • 死亡对象的回收代价是零
  • 大部分对象用过即死
  • Minor GC的时间远远低于Full GC

新生代越大越好吗?

-XMn:

堆内存大小一定,新生代内存过大,老年代内存会变小,最终导致老年代内存空间不足,导致Full GC,而Full GC的STW过长

官方建议新生代大小在堆内存25%-50之间

算法:【并发量*(请求-响应数据)】

幸存区:

  • 需要大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

    -XX:MaxTenuringThreshold=threshold

    -XX:+PrintTenuringDistribution:打印幸存区对象详细信息

老年代调优

以CMS为例

  • CMS老年代的内存越大越好
  • 先尝试不做调优,如果没有Full GC 那么已经......,否则先尝试调优新生代
  • 观察Full GC 时老年代内存占用,将老年代内存预设调大1/4~1/3
    • -XX:CMSInitiatingOccupancyFraction=percent:控制老年代垃圾占用在老年代空间中的占比为多少时进行CMS垃圾回收

案例

  1. Full GC 和 Minor GC频繁

    • 新生代内存紧张
      • 幸存区内存紧张,倒是老年代的晋升阈值降低,导致大量生命周期较短的对象直接晋升到老年代
      • 解决:调整新生代内存大小--幸存区内存增大,调整晋升阈值大小
    • 老年代内存紧张
  2. 请求高峰期发生Full GC,单次暂停时间特别长(CMS)

    • 分析哪部分STW时间较长
    • 分析GC日志,定位各阶段时间消耗情况(CMS重新标记时间较长--重新标记阶段会扫描新生代和老年代,此时新生代可能会产生大量浮动垃圾,导致分析性标记阶段时间较长)
    • 解决:在重新定位之前进行一次Minor GC,先清理一下新生代内存(-XX:+CMSScavengeBeforeRemark)
  3. 老年代充裕情况下,发生Full GC(CMS jdk1.7)

    • jdk1.8之前使用的是永久代作为方法区的实现,永久代的空间不足也会导致Full GC的出现,导致整个堆的垃圾清理
    • 1.8之后方法区使用元空间实现,元空间使用的是操作系统的内存,基本不会存在内存不足情况
    • 解决:调整永久代内存大小

类加载与字节码技术

类文件结构

javac -parameters -d . xxx.java:编译java文件

16进制查看

.class 文件在linux中使用vim打开,输入:%!xxd看到的就是16进制文件编码

00000000: cafe babe 0000 0034 001f 0a00 0600 1109  .......4........
00000010: 0012 0013 0800 140a 0015 0016 0700 1707  ................
00000020: 0018 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169  umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67  n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 104d 6574  /String;)V...Met
00000070: 686f 6450 6172 616d 6574 6572 7301 0004  hodParameters...
00000080: 6172 6773 0100 0a53 6f75 7263 6546 696c  args...SourceFil
00000090: 6501 000f 4865 6c6c 6f57 6f72 6c64 2e6a  e...HelloWorld.j
000000a0: 6176 610c 0007 0008 0700 190c 001a 001b  ava.............
000000b0: 0100 0b68 656c 6c6f 2077 6f72 6c64 0700  ...hello world..
000000c0: 1c0c 001d 001e 0100 1863 6f6d 2f79 7378  .........com/ysx
000000d0: 2f63 6865 636b 2f48 656c 6c6f 576f 726c  /check/HelloWorl
000000e0: 6401 0010 6a61 7661 2f6c 616e 672f 4f62  d...java/lang/Ob
000000f0: 6a65 6374 0100 106a 6176 612f 6c61 6e67  ject...java/lang
00000100: 2f53 7973 7465 6d01 0003 6f75 7401 0015  /System...out...
00000110: 4c6a 6176 612f 696f 2f50 7269 6e74 5374  Ljava/io/PrintSt
00000120: 7265 616d 3b01 0013 6a61 7661 2f69 6f2f  ream;...java/io/
00000130: 5072 696e 7453 7472 6561 6d01 0007 7072  PrintStream...pr
00000140: 696e 746c 6e01 0015 284c 6a61 7661 2f6c  intln...(Ljava/l
00000150: 616e 672f 5374 7269 6e67 3b29 5600 2100  ang/String;)V.!.
00000160: 0500 0600 0000 0000 0200 0100 0700 0800  ................
00000170: 0100 0900 0000 1d00 0100 0100 0000 052a  ...............*
00000180: b700 01b1 0000 0001 000a 0000 0006 0001  ................
00000190: 0000 000a 0009 000b 000c 0002 0009 0000  ................
000001a0: 0025 0002 0001 0000 0009 b200 0212 03b6  .%..............
000001b0: 0004 b100 0000 0100 0a00 0000 0a00 0200  ................
000001c0: 0000 0d00 0800 0e00 0d00 0000 0501 000e  ................
000001d0: 0000 0001 000f 0000 0002 0010 0a         .............
~                                                                

类文件结构结构


ClassFile {
    u4 magic;//文件标志
    u2 minor_version;//小版本号
    u2 major_version;//大版本号
    u2 constant_pool_count;//常量池数量
    cp_info constant_pool[constant_pool_count-1];//常量池
    u2 access_flags;//访问修饰
    u2 this_class;//当前类
    u2 super_class;//父类
    u2 interfaces_count;//接口
    u2 interfaces[interfaces_count];//一个类可以实现多个接口
    u2 fields_count;//文件的字段属性
    field_info fields[fields_count];//一个类会有多个字段
    u2 methods_count;//文件方法数量
    method_info methods[methods_count];//一个类可以有多个方法
    u2 attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}
  • 魔术信息

0~3字节,表示它是否是【class】类型文件

00000000: cafe babe 0000 0034 001f 0a00 0600 1109  .......4........

  • 版本

4~7字节,,表示类版本信息 00 34表示java8

00000000: cafe babe 0000 0034 001f 0a00 0600 1109  .......4........

  • 常量池

8~9字节表示常量池长度,001f(31),表示常量池有#1~#31项,注意#0项不计入,也没有值

00000000: cafe babe 0000 0034 001f 0a00 0600 1109  .......4........

javap -verbose xxx.class:查看常量池信息

##常量池信息
Constant pool:
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            // hello world
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            // com/ysx/check/HelloWorld
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               MethodParameters
  #14 = Utf8               args
  #15 = Utf8               SourceFile
  #16 = Utf8               HelloWorld.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Class              #25            // java/lang/System
  #19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #20 = Utf8               hello world
  #21 = Class              #28            // java/io/PrintStream
  #22 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V
  #23 = Utf8               com/ysx/check/HelloWorld
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
类  型 标   志 描  述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

访问修饰符

标志名称 标志值 含义
ACC_PLUBLIC 0x0001 表示public字段
ACC_PRIVATE 0x0002 表示private字段
ACC_PROTECTED 0x0004 表示protected字段
ACC_STATIC 0x0008 表示静态字段
ACC_FINAL 0x0010 是否为final字段,final字段表示常量
ACC_VOLATILE 0x0040 是否为volatile
ACC_TRANSIMENT 0x0080 是否为transient,表示在持久化读写时忽略该字段
ACC_SYNTHETIC 0x1000 由编译器产生的方法,没有源码对应
ACC_ENUM 0x4000 是否是枚举

字节码指令

javap -v  xxxx.class:反编译.class文件

Classfile /D:/learn/微服务/project/springboot03/src/main/java/com/ysx/check/com/ysx/check/HelloWorld.class
  Last modified 2022-7-14; size 476 bytes //476字节
  MD5 checksum 0a2135f01802ae39cedbced12513f84d//md5 校验签名
  Compiled from "HelloWorld.java"//java原文件
public class com.ysx.check.HelloWorld//类全路径名称
  minor version: 0
  major version: 52//jdk版本 1.8
  flags: ACC_PUBLIC, ACC_SUPER//访问修饰符
Constant pool://常量池
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            // hello world
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            // com/ysx/check/HelloWorld
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               MethodParameters
  #14 = Utf8               args
  #15 = Utf8               SourceFile
  #16 = Utf8               HelloWorld.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Class              #25            // java/lang/System
  #19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #20 = Utf8               hello world
  #21 = Class              #28            // java/io/PrintStream
  #22 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V
  #23 = Utf8               com/ysx/check/HelloWorld
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{//方法信息开始
  public com.ysx.check.HelloWorld();//构造方法
    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 10: 0

  public static void main(java.lang.String[]);//main方法
    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 13: 0
        line 14: 8
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "HelloWorld.java"

条件判断指令

指令 助记符 含义
0x99 ifeg 判断是否==0
0x9a ifne 判断是否!=0
0x9b ifit 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否<
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个int是否 ==
0xa6 if_acmpne 两个int是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null

说明:

  • byte,short,char都会按照int比较,因为操作数栈都是4字节
  • goto用来进行跳转到指定行号的字节码
 public static void main(String[] args) {
        int a = 0;

        if (a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
         0: iconst_0
         1: istore_1
         2: iload_1
         3: ifne          12//if判断,如果不成立,跳转到第12行指令
         6: bipush        10
         8: istore_1
         9: goto          15//goto如果不加,会继续走12行进行重新赋值
        12: bipush        20//赋值操作,赋值20
        14: istore_1
        15: return

循环控制指令

备注:实际上还是条件判断指令

public static void main(String[] args) {
        int a = 0;

        while (a < 10) {
            a++;
        }
    }
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     14
         8: iinc          1, 1
        11: goto          2
        14: return

案例:

public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i ++;
        }
        System.out.println(x);
    }
         0: iconst_0
         1: istore_1
         2: iconst_0
         3: istore_2
         4: iload_1
         5: bipush        10
         7: if_icmpge     21
        10: iload_2
        11: iinc          2, 1//在操作数栈上自增1
        14: istore_2          //从内存中获取为0,重新赋值给x
        15: iinc          1, 1
        18: goto          4
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_2
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return

构造方法

> <cinit>()v

public class Test11 {

    static int i = 10;

    static {
        i = 20;
    }

    static {
        i = 30;
    }
}

编译器会按从上至下的顺序,收集所有static静态代码块喝静态成员赋值的代码,合并为一个特殊的方法<cinit>()v

         0: bipush        10
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return

<cinit>()v方法会在类加载的初始化阶段被调用

> <init>()v

public class Test12 {

    private String a = "s1";
    {
        b = 20;
    }
    private int b = 10;

    {
        a = "s2";
    }

    public Test12(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Test12 d = new Test12("s3",30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
}

编译器会按从上至下的顺序,收集所有{}代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后

public com.ysx.check.Test12(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String s1
         7: putfield      #3                  // Field a:Ljava/lang/String;
        10: aload_0
        11: bipush        20
        13: putfield      #4                  // Field b:I
        16: aload_0
        17: bipush        10
        19: putfield      #4                  // Field b:I
        22: aload_0
        23: ldc           #5                  // String s2
        25: putfield      #3                  // Field a:Ljava/lang/String;
        28: aload_0                          //================ 愿有的构造方法====================
        29: aload_1
        30: putfield      #3                  // Field a:Ljava/lang/String;
        33: aload_0
        34: iload_2
        35: putfield      #4                  // Field b:I
                                              //======================================
        38: return
      LineNumberTable:
        line 23: 0
        line 13: 4
        line 15: 10
        line 17: 16
        line 20: 22
        line 24: 28
        line 25: 33
        line 26: 38

多态的原理

前提:jvm需要先进行禁止指针压缩操作

-XX:-UseCompressedOops -XX:-UseCompressedClassPointers

public class Test14 {

    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }

    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }

}

abstract class  Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是"+this.getClass().getSimpleName();
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("啃骨头");
    }
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("吃鱼");
    }
}

jps查看进程号

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB:工具虚拟机底层的内存操作和地址

前提:需要在jdk根目录下运行

异常处理

public static void main(String[] args) {

        int i = 0;

        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }

    }

javap结果

stack=1, locals=3, args_size=1
         0: iconst_0 //0变量
         1: istore_1//赋值到局部变量表1槽位中
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2//把异常变量的引用地址存储到局部变量表异常变量槽位2中
         9: bipush        20
        11: istore_1
        12: return
         Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception//监听2,4行代码,如果发生异常且异常类型为监听异常,跳到第八行代码
      LineNumberTable:
        line 15: 0
        line 18: 2
        line 21: 5
        line 19: 8
        line 20: 9
        line 23: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/Exception; //异常局部变量
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I
  • 一个Exception table的结构,[from,to)是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号
  • 8行的字节码指令astore_2是蒋异常对象的引用存入局部变量表的slot 2位置

> 多个single-catch

 public static void main(String[] args) {

        int i = 0;

        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 20;
        } catch (NullPointerException e) {
            i = 30;
        } catch (Exception e) {
            i = 40;
        }

    }

javap编译结果

 stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        20
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        30
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        40
        25: istore_1
        26: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException//多个exception会根据type进行异常捕捉
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
      LineNumberTable:
        line 15: 0
        line 18: 2
        line 25: 5
        line 19: 8
        line 20: 9
        line 25: 12
        line 21: 15
        line 22: 16
        line 25: 19
        line 23: 22
        line 24: 23
        line 27: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;//同时只存在一个异常对象,所以存在槽位服用
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
  • 因为异常出现时,只能进入Exception table中一个分支,所以局部变量slot 2 位置被共用

> multi-catch

finally

public static void main(String[] args) {

        int i = 0;

        try {
            i = 10;
        } catch (Exception e) {
            i = 40;
        } finally {
            i = 50;
        }

    }
 stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10//放入操作数栈栈顶
         4: istore_1
         5: bipush        50//finally代码
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        40
        14: istore_1
        15: bipush        50//finally代码
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        50//finally代码
        24: istore_1
        25: aload_3
        26: athrow 
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any //剩余的异常类型,比如Error 监听try块中异常(异常类型为Exception同级或父级异常出现时,Exception无法捕获)
            11    15    21   any //剩余的异常类型,比如Error 监听catch块中异常
      LineNumberTable:
        line 15: 0
        line 18: 2
        line 22: 5
        line 23: 8
        line 19: 11
        line 20: 12
        line 22: 15
        line 23: 18
        line 22: 21
        line 23: 25
        line 25: 27
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I

可以看到finally中的代码被复制了三份,分别放入try流程,catch流程以及catch剩余的异常类型流程

synchronized 代码块

public static void main(String[] args) {
        Object lock = new Object();

        synchronized (lock) {
            System.out.println("ok");
        }
    }
stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup             //复制对象引用
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1        //lock引用-> lock
         8: aload_1         //对象引用压入操作数栈
         9: dup             
        10: astore_2        //lock引用-> slot 2
        11: monitorenter    //lock引用加锁操作
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2        //<-slot 2(lock引用)
        21: monitorexit    //monitorexit(lock引用)解锁
        22: goto          30
        25: astore_3       //any -> slot 3
        26: aload_2        //<-slot 2(lock引用)
        27: monitorexit    //monitorexit(lock引用)解锁
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 13: 0
        line 15: 8
        line 16: 12
        line 17: 20
        line 18: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

注意:方法级别的synchronized不会再字节码指令中有所体现

编译期处理

所谓的 语法糖,其实就是指java编译器把.Java源码编译为.class字节码的过程中,自动胜场和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利

自动拆装箱

此特性时jdk 5开始加入的,代码     片段:

 public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }

这段代码再jdk 5之前是无法编译通过的,必须改写为代码  片段2:

 public static void main(String[] args) {
        Integer x = Integer.valueOf(1);
        int y = x.intValue();
    }

之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在jdk 5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段被转换为代码片段2

泛型结合取值

泛型也是jdk 5 开始加入的特性,但java在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码后就丢失了,实际的类型都当作了Object类型来处理

 public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10);//实际调用的是list。add(Object e)
        Integer x = list.get(0);//实际调用的是Object obj = List.get(int index)
    }

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作

//需要蒋Object转为Integer
Integer x = (Integer) list.get(0)

如果前面的x变量类型修改为int基本类型那么最终生成的字节码是:

//需要蒋Object转为Integer,并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
 stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable:
        line 17: 0
        line 18: 8
        line 19: 20
        line 20: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
     LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;

可变参数

可变参数也是jdk 5 开始加入的新特性

public static void foo(String ...args) {

    }

public static void foo(String[] args) {

    }

其中String ...args在编译期间会变化为String[] args

注意:

如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建一个空数组,而不会传递null值

foreach 循环

public static void main(String[] args) {
        int[] array = {1,2,3,4,5};//数组赋初值的简化写法也是语法糖
        for (int i : array) {
            System.out.println(i);
        }
    }

被编译器转换后为:

public static void main(String[] args) {
        int[] array = new int[]{1,2,3,4,5};
        for (int i = 0; i < array.length; i++) {
            int e = array[i];
            System.out.println(e);
        }
    }

针对List集合

 List<Integer> list  = Arrays.asList(1,2,3,4,5);
        for (Integer i : list) {
            System.out.println(i);
        }

被编译器转换后为:

List<Integer> list  = Arrays.asList(1,2,3,4,5);
        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            Integer e = (Integer)iter.next();
            System.out.println(e);
        }

注意:

foreach循环写法,能够配合数组,以及所有实现了Iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用c++的instanceKlass描述类,它的重要field有:

    • _java_mirror即java的类镜像,例如对String来说,就是String.class,作用是把class暴露给java使用
    • _super即父类
    • _fields即成员变量
    • _methods即方法
    • _constants即常量池
    • _class_loader即类加载器
    • _vtable虚方法表
    • _itable接口方法表
  • 如果这个类还有父类没有加载,先加载父类

  • 加载和链接可能是交替运行的

注意:

  • instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中
  • 可以通过前面介绍的HSDB查看

链接

  • 验证:验证类是否符合 JVM 规范,安全性检查

  • 准备:为static变量分配空间,设置默认值

    • static变量在jdk 7之前存储于instanceKlass末尾,从jdk7开始存储于_java_mirror末尾

    • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

    • 如果static变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

    • 如果static变量是final,但属于引用类型,那么赋值也会在初始化阶段完成

public class Test18 {
    static int a;
    static int b = 10;
    static final int c = 20;
}
 static int a;//只是声明,没有赋值
    descriptor: I
    flags: ACC_STATIC

  static int b;//声明且赋值
    descriptor: I
    flags: ACC_STATIC

  static final int c;//final变量在编译时就确定了值,所以在编译期就可以直接赋值
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20

  public com.ysx.check.Test18();
    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 15: 0

  static {};//在调用类的构造方法时,进行b变量的赋值
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: return
      LineNumberTable:
        line 17: 0
static int a;//只是声明,没有赋值
    descriptor: I
    flags: ACC_STATIC

  static int b;//声明且赋值
    descriptor: I
    flags: ACC_STATIC

  static final int c;//final变量在编译时就确定了值,所以在编译期就可以直接赋值
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20

  static final java.lang.String d; //final变量在编译时就确定了值,所以在编译期就可以直接赋值
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String aaaaaaaa

  static final java.lang.Object e;//引用对象虽然是final但是无法确定值,需要在初始化阶段赋值
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL

  public com.ysx.check.Test18();
    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 15: 0

  static {};//在调用类的构造方法时,进行b变量的赋值
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: new           #3                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putstatic     #4                  // Field e:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 17: 0
        line 20: 5
  • 解析:将常量池中的符号引用解析为直接引用
public class Test18 {

    public static void main(String[] args)throws ClassNotFoundException, IOException {
ClassLoader classLoader = Test18.class.getClassLoader();

        //loadClass方法不会导致类的解析和初始化
        Class<?> e = classLoader.loadClass("com.ysx.check.E");

        //会导致类的加载和解析,同时也会触发类中引用对象的加载和解析
        new E();
        System.in.read();

    }
}

class E {
    F f = new F();
}

class F {

}

注意:

  • 在使用loadClass进行加载时,没有初始化和解析E对象,此时E中的f对象仅仅只是一个字符,没有与之匹配的内存地址,处于未解析的对象状态
  • 使用new()时会导致f对象的加载和解析,此时f是作为一个对象被加载到内存中

初始化

<cinit>()V方法

初始化即调用<cinit>()V,虚拟机会保证这个类的【构造方法】的线程安全

发生的时机

概括的说,类初始化是【懒惰的】

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发父类初始化
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new会导致初始

不会导致初始化的情况

  • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName的参数2为false时
public class Test18 {
    static {
        System.out.println("init main");
    }

    //main方法所在的类总是最先被初始化的
    public static void main(String[] args)throws ClassNotFoundException, IOException {
        //不会造成类初始化的情况
        //访问类的静态变量不会导致初始化
//        System.out.println(F.b);
        //类对象的.class不会触发初始化
//        System.out.println(F.class);
        //创建该类的数组不会触发初始化
//        System.out.println(new F[0]);
        //类加载器的loadClass不会触发初始化
//        ClassLoader loader = Thread.currentThread().getContextClassLoader();
//        loader.loadClass("com.ysx.check.F");
        //Class.forName的参数2为false时
//        ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("com.ysx.check.F",false,loader2);

        //会造成类初始化的情况
        //首次访问这个类的静态变量或静态方法时
//        System.out.println(E.a);
        //子类初始化,如果父类还没初始化,会引发父类初始化
//        Class.forName("com.ysx.check.F");
        //子类访问父类的静态变量,只会触发父类的初始化
//        System.out.println(F.a);
        //Class.forName
//        Class.forName("com.ysx.check.E");

        //new 方法
//        new E();

    }

}

class E {
    public static  int a  = 10;
    static {
        System.out.println("init E");
    }

}

class F extends E{

    public static final double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("init F");
    }
}

练习1:

public class Test18 {

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

//        System.out.println(E.a);
//        System.out.println(E.b);

        System.out.println(E.c);

    }

}

class E {
    public static final int a  = 10;
    public static final String b = "1111";
    public static final Integer c  = 20;

    static {
        System.out.println("init E");
    }

}

判断上述三个操作是否会导致类的初始化

结果:

a和b属性属于静态常量且符合基本类型和字符串的定义,在编译期就会进行赋值,不会导致初始化

c属性虽然是静态常量,但是不是基本类型,所以它的赋值是在类的初始化阶段进行操作的

联系2:

完成懒惰初始化单例模式

public class Test19 {

    public static void main(String[] args) {
        SingleTon.getInstance();
    }

}
class SingleTon {

    private SingleTon() {

    }

    private static class LazyHolder {
        private static final SingleTon SINGLETON = new SingleTon();
        static {
            System.out.println("LazyHolder 初始化");
        }
    }

    public static SingleTon getInstance() {
        return LazyHolder.SINGLETON;
    }

    public static void test() {
        System.out.println("test");
    }
}

注意:

由类加载器操控,可以保证线程安全

类加载器

jdk 1.8 为例:

名称 加载哪的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader(扩展类加载器) JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application ClassLoader(应用程序类加载器) classpath 上级为Extension
自定义类加载 自定义 上级为Application

启动类加载器

用 Bootstrap类加载器加载类:

public class Test1 {

    static {
        System.out.println("bootstrap Test1 init");
    }
}

执行

public class Test2 {

    public static void main(String[] args) throws ClassNotFoundException{
        Class<?> aClass = Class.forName("com.ysx.check.loader.Test1");
        System.out.println(aClass.getClassLoader());
    }
}
D:\learn\微服务\project\springboot03>java -Xbootclasspath/a:. com.ysx.check.loader.Test2

输出

bootstrap Test1 init
null
  • -Xbootclasspath表示设置bootclasspath
  • 其中/a:.表示当前目录追加至bootclasspath后
  • 可以用这个办法替换核心类
    • Java -Xbootclasspath:<new bootclasspath>
    • Java -Xbootclasspath/a: <new bootclasspath>
    • Java -Xbootclasspath/p: <new bootclasspath>

扩展类加载器

public class Test1 {

    static {
        System.out.println("classpath Test1 init");
    }
}

执行

public class Test2 {

    public static void main(String[] args) throws ClassNotFoundException{
        Class<?> aClass = Class.forName("com.ysx.check.loader.Test1");
        System.out.println(aClass.getClassLoader());
    }
}

输出

classpath Test1 init
sun.misc.Launcher$AppClassLoader@18b4aac2

使用的是应用程序加载器加载的Test1

public class Test1 {

    static {
        System.out.println("ext Test1 init");
    }
}

使用命令:jar -cvf my.jar com\ysx\check\loader\Test1.class 打完jar包后入C:\Program Files\Java\jdk1.8.0_291\jre\lib\ext下,重新运行

输出:

ext Test1 init
sun.misc.Launcher$ExtClassLoader@36d64342

此时使用的是扩展类加载器加载

双亲委派机制

所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则

注意:

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

源码分析

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //1.检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //启动类加载器在java获取为null,如果parent为null,则证明此时为扩展类加载器
                    if (parent != null) {
                        //2.有上级的话,委派上级loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //3.如果没有上级了(ExtClassLoader),则委派BootStrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.

                    long t1 = System.nanoTime();
                     //4.每一层都找不到,调用findClass方法(每个类加载器自己扩展)来加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    //5.记录耗时
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

线程上下文类加载器

我们再使用JDBC时,都需要加载Driver驱动,即使不屑

Class.forName("com.mysql.jdbc.Driver")

也是可以让com.mysql.jdbc.Driver正确加载的,你知道是怎么做的吗?

根据源码分析:

public class DriverManager {

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    private static volatile int loginTimeout = 0;
    private static volatile java.io.PrintWriter logWriter = null;
    private static volatile java.io.PrintStream logStream = null;
    // Used in println() to synchronize logWriter
    private final static  Object logSync = new Object();

    /* Prevent the DriverManager class from being instantiated. */
    private DriverManager(){}

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {home.php?mod=space&uid=295005 ServiceLoader} mechanism
     */
    static {
        //加载初始化驱动
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

DriverManager的类加载器使用的是启动类加载器,会到JAVA_HOME\jre\lib下搜索类,但是JAVA_HOME\jre\lib下显然没有mysql-connector-java-5.1.47.jar包,那么,在DriverManager的静态代码块种,怎么能正确加载com.mysql.jdbc.Driver?

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

       //1、使用ServiceLoader机制加载驱动,即SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        //2、使用jdbc.drivers定义驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                //这里ClassLoader.getSystemClassLoader()就是应用程序类加载器,此时jdk打破了双亲委派机制,因为有些类在启动类加载器路径下找不到
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

ServiceLoader

Service Provider Interface (SPI)

约定如下:在jar包的MATE-INF/services 包下,以接口权限定义名为文件,文件内容是实现类名称

这样就可以使用

ServiceLoader<接口类型> loadedDrivers = ServiceLoader.load(接口类型.class);
                Iterator<接口类型> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }

来得到实现类,体现的是【面向接口变成+解耦】的思想,在下面的一些框架中都运用了此思想

  • JDBC
  • Service初始化器
  • Spring容器
  • Dubbo(对SPI进行了扩展)
public static <S> ServiceLoader<S> load(Class<S> service) {
        //得到当前线程的类加载器,称为线程上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName调用了线程上下文类加载器完成类加载

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

自动有类加载器

场景:

  • 想加载非classpath随意路径中的类文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器

步骤:

  1. 继承ClassLoader父类
  2. 要遵从双亲委派机制,重写findClass方法
    • 注意不是重写loadClass方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的defineClass方法来加载类
  5. 使用者调用该类加载器的loadClass方法

示例:

public class TestLoader {

    public static void main(String[] args) throws Exception {
        LoaderTest loaderTest = new LoaderTest();

        Class<?> c1 = loaderTest.loadClass("Test1");
        Class<?> c2 = loaderTest.loadClass("Test1");
        System.out.println(c1 == c2);

        LoaderTest loaderTest2 = new LoaderTest();
        Class<?> c3 = loaderTest2.loadClass("Test1");

        System.out.println(c1 == c3);
    }
}

class LoaderTest extends ClassLoader {

    @Override//name就是类名称
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "D:\\test\\"+name+".class";
        ByteArrayOutputStream os = null;
        try {
            os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path),os);

            byte[] bytes = ((ByteArrayOutputStream) os).toByteArray();
            return defineClass(name,bytes,0,bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到");
        }
    }
}

运行期优化

即时编译

分层编译

public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {

            long start = System.nanoTime();

            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }

JVM将执行状态分成了五个层次:

  • 0层,解释执行(Interpreter)
  • 1层,使用C1即时编译器编译执行(不带profiling)
  • 2层,使用C1即时编译器编译执行(带基本的profiling)
  • 3层,使用C1即时编译器编译执行(带完全的profiling)
  • 4层,使用C2即时编译器编译执行
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT是将一些字节码编译为机器码,并存储Code Cache,下次再遇到相同代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用diamond,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度,执行效率上简单比较一下Interpreter< C1 < C2总的目标是发现热点代码(hotspot名称的又来),进行优化

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用-XX:-DoEscapeAnalysis关闭逃逸分析,再运行刚才的示例观察结果

方法内联

(Inlining)

private static int square(final int i) {
        return i * i;
    }
System.out.println(square(9));

如果发现square是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置

System.out.println(9*9);

还能进行常量折叠(constant folding)的优化

System.out.println(81);

-XX:+UnlockDiagnosticVMOptions  -XX:+PrintInlining (解锁隐藏参数)打印inlining信息

-XX:CompileCommand=dontinline,Test3.square 禁用方法内联

-XX:+PrintCompilation

public static void main(String[] args) {

        int x = 0;
        for (int i = 0; i < 200; i++) {

            long start = System.nanoTime();

            for (int j = 0; j < 1000; j++) {
                x = square(9);
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }

字段优化

JVM 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh

反射优化

public class Reflect1 {

    public static void foo() {
        System.out.println("foo ......");
    }

    public static void main(String[] args) throws Exception {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t",i);
            foo.invoke(null);
        }
        System.in.read();

    }
}

foo.invoke前面0~15此调用使用的是MethodAccessor的NativeMethodAccessorImpl(本地方法访问器)实现

源码分析

@CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }
class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        //当本地方法的invoke调用次数超过ReflectionFactory.inflationThreshold()时就会把本地方法访问器替换为运行期间动态生成的方法访问器
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());

            //替换原本的NativeMethodAccessorImpl
            this.parent.setDelegate(var3);
        }

        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

动态生成的方法访问器

public class GeneratorMethodAccessor1 extend MethodAccessorImpl{

    public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
        //比较奇葩的做法,如果由参数,那么抛非法参数异常
      block4 : {
          if(arrobject == null || arrobject.length == 0) 
              throw new IllegalArgumentException();
      }
        try {
            //此时,已经是直接调用了
            Reflect1.foo();
            //没有返回值
            return null;
        } catch (Throwable throwable) {
            throw new InvocationTargetException();
        } catch (ClassCastException | NullPointerException runtimeException) {
            throw new IllegalArgumentException(Object.super.toString());
        }
    }

}

在0-15次循环过程中,jvm都是通过反射调用执行foo()方法,但是在第16次及其以后是由动态生成的方法访问器进行invoke调用,此时是直接使用对象.方法(Reflect1.foo)进行调用,

public class ReflectionFactory {
    private static boolean initted = false;
    private static final Permission reflectionFactoryAccessPerm = new RuntimePermission("reflectionFactoryAccess");
    private static final ReflectionFactory soleInstance = new ReflectionFactory();
    private static volatile LangReflectAccess langReflectAccess;
    private static volatile Method hasStaticInitializerMethod;
    private static boolean noInflation = false;
    private static int inflationThreshold = 15;//膨胀阈值

    private static void checkInitted() {
        if (!initted) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.out == null) {
                        return null;
                    } else {
                        String var1 = System.getProperty("sun.reflect.noInflation");
                        if (var1 != null && var1.equals("true")) {
                            ReflectionFactory.noInflation = true;
                        }

                        var1 = System.getProperty("sun.reflect.inflationThreshold");
                        if (var1 != null) {
                            try {
                                ReflectionFactory.inflationThreshold = Integer.parseInt(var1);
                            } catch (NumberFormatException var3) {
                                throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", var3);
                            }
                        }

                        ReflectionFactory.initted = true;
                        return null;
                    }
                }
            });
        }
    }

}

注意:

通过查看ReflectionFactory源码得知

  • sun.reflect.noInFlation可以用来禁用膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较好事,如果仅反射调用一次,不划算)

  • sun.reflect.inflationThreshold可以修改膨胀阈值
  • 也可以修改sun.reflect.noInflation禁用膨胀

内存模型

很多人将Java内存结构java内存模型分不清,【Java 内存模型】是java Memory Model(JMM)

简单的说,JMM定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。

问题

synchronized(同步关键字)

synchronized (对象) {
    要作为原子操作代码
}

解决方法

public class Test5 {
    static int i = 0;
    static Object obj = new Object();

    public static void main(String[] args)throws InterruptedException {
        Thread s1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (obj) {
                    i++;
                }
            } 
        });
        Thread s2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (obj) {
                    i--;
                }
            }
        });
        s1.start();
        s2.start();

        s1.join();
        s2.join();
        System.out.println(i);

    }

}

原子性

        static int i = 0;
    static Object obj = new Object();

    public static void main(String[] args)throws InterruptedException {
        Thread s1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                    i++;
            }
        });
        Thread s2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                    i--;
            }
        });
        s1.start();
        s2.start();

        s1.join();
        s2.join();
        System.out.println(i);

    }

以上结果可能是正数、负数、零,因为java 中对静态变量的自增,自减并不是原子操作,例如对i++而言,实际会产生如下的JVM指令

getstatic        i//获取静态变量1的值
iconst_1         //准备常量1
iadd                 //自增
putstatic   i//将修改后的值存入静态变量

而对应i--也是类似的:

getstatic        i//获取静态变量1的值
iconst_1         //准备常量1
isub                 //自减
putstatic   i//将修改后的值存入静态变量

而java 的内存模型如下,完成静态变量的自增,自减需要再主存和线程内存中进行数据交换

详情查看:JMM内存模型.vsd

如果是单线程以上8行代码是顺序执行没有问题;

getstatic        i//线程1-》获取静态变量1的值i = 0
iconst_1         //线程1-》准备常量1
iadd                 //线程1-》自增i = 1
putstatic   i//线程1-》将修改后的值存入静态变量 i = 1
getstatic        i//线程1-》获取静态变量1的值 i = 1
iconst_1         //线程1-》准备常量1
isub                 //线程1-》自减i = 0
putstatic   i//线程1-》将修改后的值存入静态变量 i = 0

但是多线程下这8行代码可能会交错运行:

出现负数的情况

//假设i的初始值为0
getstatic        i//线程1-》获取静态变量1的值 i = 0
getstatic        i//线程2-》获取静态变量1的值 i = 0
iconst_1         //线程1-》准备常量1
iadd                 //线程1-》自增i = 1
putstatic   i//线程1-》将修改后的值存入静态变量 i = 1
iconst_1         //线程2-》准备常量1
isub                 //线程2-》自减 线程内 i = -1
putstatic   i//线程2-》将修改后的值存入静态变量 i= -1

出现正数的情况

//假设i的初始值为0
getstatic        i//线程1-》获取静态变量1的值 i = 0
getstatic        i//线程2-》获取静态变量1的值 i = 0
iconst_1         //线程1-》准备常量1
iadd                 //线程1-》自增i = 1
iconst_1         //线程2-》准备常量1
isub                 //线程2-》自减 线程内 i = -1
putstatic   i//线程2-》将修改后的值存入静态变量 i= -1
putstatic   i//线程1-》将修改后的值存入静态变量 i = 1

可见性

存在一个现象,mainxia昵称对run变量的修改对于t线程不可见,导致t线程无法停止

 static boolean run = true;

    public static void main(String[] args)throws InterruptedException {
       Thread t = new Thread(() -> {
           while (run) {
               //...
           }
       });
       t.start();
       Thread.sleep(1000);
       run = false;//线程t不会如预想的停止

    }

分析

  1. 初始状态,t线程刚开始从主内存读取run的值到达工作内存
  2. 因为t线程需要频繁的从主内存中读取run的值,JIT编译器会将run的值缓存到自己工作内存中的高速缓存中,减少对主内存中run的访问,提高效率
  3. 1秒后,main线程修改了run的值,并同步至主存,而t是从自己工作内存的高速缓存中来读取这个变量的值,结果永远是旧值

解决

volatile(易变关键字)

它可以修饰成员变量喝静态成员变量,它可以避免线程从自己的工作内存中读取静态变量的值,必须要到主存中获取它的值,线程操作volatile变量都是直接操作主存

定义:保证在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况

getstatic        run//线程t-》获取静态变量run的值 run true
getstatic        run//线程t-》获取静态变量run的值 run true
getstatic        run//线程t-》获取静态变量run的值 run true
getstatic        run//线程t-》获取静态变量run的值 run true
getstatic        run//线程t-》获取静态变量run的值 run true
putstatic   run//线程 main 修改 run 值为 false
getstatic        run//线程t-》获取静态变量run的值 run true

注意:

  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对较低

如果在前面的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,为什么?

//println中使用synchronized,保证了t线程只能从主内存中读取变量值
public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

有序性

public class Test5 {
    int num = 0;
    boolean ready = false;

    //线程1执行此方法
    public void actor1(IResult r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    //线程2执行此方法
    public void actor2(IResult r) {
        num = 2;
        ready = true;
    }
}

class IResult {
    public static int r1;
}

问结果存在几种?

  1. 线程1先执行,此时reday = false,所以进入else分支,结果为1;
  2. 线程2先执行num = 2,但还没来得及执行 ready = true,线程1执行,还是进入else,结果为1;
  3. 线程2执行到ready = true,线程1执行,此时进入if分支,结果为4

但是可能还存在线程2执行ready = true,切换到线程1,进入if分支,相加为0,再切回线程2执行num = 2

这种现象叫做指令重排,是JIT编译器在运行时的一些优化,这个现象需要大量测试才能复现:

可以借助 java 并发压测工具 jcsstess复现:http://wiki.openjdk.java.net/display/CodeTools/jcstress

解决

volatile可以禁用指令重排

理解

同一个线程内,JVM会在不影响正确性的前提下,可以调整预警的执行顺序,由于不会对最终结果产生影响,可能会对代码的执行顺序进行调整

这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如著名的double-checked locking(双重校验锁)模型实现单例

final class SingleTon {
    private SingleTon() {}

    private static SingleTon INSTANCE = null;

    public static SingleTon getInstance() {
        //实例没创建,才会进入内部的synchronized代码块
        if (INSTANCE == null) {
            synchronized (SingleTon.class) {
                //也许有其他线程已经创建实例,所以再判断一次
                if (INSTANCE == null) {
                    INSTANCE = new SingleTon();
                }
            }
        }
        return INSTANCE;
    }

}

特点:

  • 懒惰实例化
  • 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁

但是在多线程环境下,上面的代码是有问题的,INSTANCE = new SingleTon();对应的字节码为:

0:new                                #2        //Class com/ysx/check/loader/SingleTon
3:dup                                
4:invokespecial                #3        //Method "<init>":()V
7:putstatic                        #4        Field INSTANCE:L com/ysx/check/loader/SingleTon;

其中4,7两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1,t2按如下时间序列执行

1        t1线程执行到 INSTANCE = new SingleTon() ;
2        t1线程分配空间,为SingleTon对象生成了引用地址(0)
3        t1线程将引用地址赋值给 INSTANCE,此时 INSTANCE != null(7)
4        t2线程进入getInstance()方法,发现 INSTANCE != null (synchronized块外),直接返回INSTANCE
5        t1线程执行SingleTon构造方法(4)

此时t1还未完全将构造方法执行完毕,如果再构造方法中要执行很多的初始化操作,那么t2拿到的将是一个未初始化完全的单例

解决:

INSTANCE 变量使用volatile变量修饰即可,可以禁用指令重排,但要注意在JDK 5以上版本的volatile才会有效

happens-before

happens-before规定了哪些写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结

  • 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
        static  int x;
    static  Object m = new Object()

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (m) {
                x = 10;
            }
        },"t1").start();
       new Thread(() -> {
            synchronized (m) {
                System.out.println(x);
            }
        },"t2").start();

    }
  • 线程对volatile变量的写,对接下来其他线程对该变量的读可见

  • 线程start前对变量的写,对该线程开始后对该变量的读可见

static  int x;

    public static void main(String[] args) {
       x = 20;
       new Thread(() -> {
                System.out.println(x);
        },"t2").start();

    }
  • 线程结束前的写,对其他线程得知它结束后的读可见
 static  int x;

    public static void main(String[] args)throws InterruptedException {
        Thread t1 = new Thread(() -> {
                System.out.println(x);
        });

        t1.start();
        t1.join();
        System.out.println(x);
    }
  • 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被大端口对变量的读可见
static  int x;

    public static void main(String[] args)throws InterruptedException {
        Thread t2 = new Thread(() -> {
            while (true) {
                //如果线程被打断后,跳出循环
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        });
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x = 10;
            //打断t2
            t2.interrupt();
        },"t1").start();

        while (!t2.isInterrupted()) {
            Thread.yield();
        }
        System.out.println(x);
    }
  • 对变量默认值(0,false,null) 的写,对其他线程对该变量的读可见
  • 具有传递性,如果x hb- > y 并且 y hp- > z ,那么有x hp- > z

CAS 与原子类

CAS

CAS 即 Compare and Swap,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行+1操作;

//需要不断尝试 
while (true) {
            int 旧值 = 共享变量;//比如拿到了当前的值 0 
            int 结果 = 旧值 +1;//在旧值 0 的基础上增加了 1 ,正确结果是 1
            /*
            此时如果别的线程把共享变量改成了 5 ,本线程的正确结果 1 就报废了,这时候 compareAndSwap 返回false,重新尝试
            ,直到:
            compareAndSwap 返回true,表示我本线程做修改的同时,别的线程没有干扰

            */
            if (compareAndSwap(旧值,结果)) {
                //成功,退出循环

            }
 }

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。

  • 没有使用synchronized,所以线程不会陷入阻塞,这是效率提升因素之一
  • 但是如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,具体可查看多线程高级并发JUC.md

乐观锁和悲观锁

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃点亏再重试
  • synchronized是基于悲观锁的思想:最悲观的估计,需要防范其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁以后你们才能改

原子类

原子操作类:jdk1.5以后提供了juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用了CAS技术+volatile来实现的

例如:

//创建原子整数对象
    private static AtomicInteger x = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                x.getAndIncrement();//获取并且自增 i++
                //x.incrementAndGet() ;//自增并且获取 ++i; 

            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                x.getAndDecrement();//获取并自减 i--
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(x);

    }

synchronized 优化

java HotSpot虚拟机中,每个对象都有对象头(包括class指针和Mark Word),Mark Word平时存储这个对象的 哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为 标记位、线程锁记录指针、重量级锁指针、线程ID等内容

synchronized-轻量级锁

每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

static Object obj = new Object();

    public static void method() {
        synchronized (obj) {
            //同步块A
            method2();
        }
    }

    public static void method2() {
        synchronized (obj) {
            //同步块B
        }
    }
线程1 对象Mark Word 线程2
访问同步块A,把Mark复制到线程1的锁记录 01(无锁)
CAS修改Mark为线程1锁记录地址 01(无锁)
成功(加锁) 00(轻量锁)线程1锁记录地址
执行同步块A 00(轻量锁)线程1锁记录地址
访问同步块B,把Mark 复制到线程1的锁记录 00(轻量锁)线程1锁记录地址
CAS修改Mark为线程1锁记录地址 00(轻量锁)线程1锁记录地址
失败(发现是自己线程的锁) 00(轻量锁)线程1锁记录地址
锁重入 00(轻量锁)线程1锁记录地址
执行同步块B 00(轻量锁)线程1锁记录地址
同步块B执行完毕 00(轻量锁)线程1锁记录地址
同步块A执行完毕 00(轻量锁)线程1锁记录地址
成功(解锁) 01(无锁)
01(无锁) 访问同步块A,把Mark复制到线程1的锁记录
00(轻量锁)线程2锁记录地址 CAS修改Mark为线程2锁记录地址
00(轻量锁)线程2锁记录地址 成功(加锁)
00(轻量锁)线程2锁记录地址 执行同步块A
00(轻量锁)线程2锁记录地址 访问同步块B,把Mark 复制到线程2的锁记录
00(轻量锁)线程2锁记录地址 CAS修改Mark为线程2锁记录地址
00(轻量锁)线程2锁记录地址 失败(发现是自己线程的锁)
00(轻量锁)线程2锁记录地址 锁重入
00(轻量锁)线程2锁记录地址 执行同步块B
00(轻量锁)线程2锁记录地址 同步块B执行完毕
00(轻量锁)线程2锁记录地址 同步块A执行完毕
01(无锁) 成功(解锁)

锁膨胀

如果在尝试轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁。

static Object obj = new Object();

public static void method() {
    synchronized (obj) {
        //同步块
    }
}

重量级锁

重量级锁竞争的时候,还可以使用自旋进行优化,如果当前线程自旋成功(即这时候持锁线程一句退出了同步块,释放了锁),这时当前线程就可以避免阻塞

在java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,需要在多核CPU场景下才能发挥优势
  • 好比等红灯时汽车不是熄火,不熄火相当于自旋(等待短了时间划算,减少了线程上下文切换的时间),熄火相当于阻塞(时间长了划算)
  • java7以后不能控制是否开启了自旋功能

其他优化

  • 减少上锁时间:同步代码块中尽量短
  • 减少锁的粒度:将一个锁分为多个锁提高并发度
    • ConcurrentHashMap
    • LongAdder分为base和cells两部分。没有并发争用的时候或是cells数组正在初始化的时候,会使用CAS来累加值到base,有并发争用,会初始化cells数组,数组有多少个cell,就允许有多少线程冰心修改,最后将数组中每个cell累加,再加上base就是最终的值
    • LinkedBlockingQueue入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
  • 锁粗化;多次循环进入代码块不如同步块内多次循环,另外JVM可能会做如下优化,,把多次append的加锁操作粗话为一次(因为都是对同一个对象加锁,没必要重入多次)
    • new StringBuffer().append("a").append("b").append("c")
  • 锁消除:JVM会进行代码的逃逸分析,例如某个加锁对象时方法内局部变量,不会被其他线程所访问到,这时候就会被即时编译器忽略掉所有同步操作
  • 读写分离:
    • CopyOnWriteArrayList
    • CopyOnWriteSet


如果嫌太长可以下载zip的包,对应的流程图vsd都在里面,想学习的可以看下

jvm.zip

311.54 KB, 下载次数: 34, 下载积分: 吾爱币 -1 CB

md文件和流程图文件

免费评分

参与人数 4吾爱币 +4 热心值 +4 收起 理由
阿隆 + 1 + 1 我很赞同!
bbs119 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ffuujian + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
bug0593 + 1 + 1 热心回复!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

sacudir 发表于 2022-8-4 15:18
技术贴,学习下
yy6353393 发表于 2022-8-4 15:34
W2j000 发表于 2022-8-4 16:03
lxn13393617553 发表于 2022-8-4 16:06
感谢楼主分享,又是学习的一天
ffuujian 发表于 2022-8-4 16:36
楼主能提供视频学习下么
ADlance76 发表于 2022-8-4 16:37
感谢大佬分享。
在校期间没有好好学习java,现在来补习真的是补到头秃
uze52pojie 发表于 2022-8-4 16:51
支持一下
bbs119 发表于 2022-8-4 16:55
这个厉害了
xiaochuan827 发表于 2022-8-4 16:56
感谢,学习了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止回复与主题无关非技术内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-5-15 14:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表