JVM基础(一)

160 阅读18分钟

JVM_01

  • 推荐阅读

    • Java虚拟机规范(中文版译本)基于Java7:适合Android程序员
    • 深入理解虚拟机规范:进阶
  • JVM是一种规范

    • 更加强调约定的味道:只要为满足Java虚拟机规范的 .class 文件,JVM都可以执行
    • 这点在JVM的语言无关性中有所体现
  • JDK包含:

    • JRE:Java运行时环境,没有编译功能的,=JVM+Java类库
    • JVM:虚拟机
    • 某些工具:jhat.exe,这玩意儿在bin目录下有
  • Java程序执行流程

    1. 源程序(.java)经过编译(javac)到字节码文件(.class)

    2. (满足Java虚拟机规范的)交由 JVM处理

    3. JVM处理:

      • 类加载器加载
      • 执行(字节码解释器,JIT编译器)
      • 执行引擎->硬件设备
  • 解释执行(字节码解释器)

    • JVM是用C++编写的,JVM相当于拿到合乎规范的字节码文件后交由C++解释器进行处理

    • 绝大多数代码执行,效率就比较慢

    • C++解释器:JVM帮助解释

      • 实际上就是一系列的 if 语句

         //可能是汇编等其他的,具体要看这个解释器
         if(new){
             相应的C++代码,完成java中的new操作; 
         }
        
  • JVM的执行:

    • 分为客户端和服务端
  • 即时编译(JIT热点代码技术)

    • 当方法、代码的循环次数达到一定量(1W+):走热点编译(JIT,Hotspot)

    • 此时不再经过JVM转化,直接从codecache(java程序编译的时候就比较慢了)里面拿出来就用了

    • 关于JVM转换:这个还有点问题

      • Java代码->字节码->(这步骤就很费时了)汇编->机器码
      • 一般翻译成汇编就行了
  • JVM的跨平台特性

    • 不同的平台都有相对应的JVM版本
    • Windows,Linux,Unix,Android,Mac
  • JVM的语言无关性:语言只是将程序翻译为字节码文件

    • JVM处理的原材料是符合Java虚拟机规范的字节码文件,至于它是怎么来的,不管
    • JVM系语言:Java,kotlin,scala,grooxy
  • JVM的跨平台行+语言无关性决定了生态圈比较大

  • 常见的JVM实现:多种实现方式

    • 自己写个JVM实现

      1. 遵循Java虚拟机规范(底层实现无限制)
      2. 得到Oracle认证
      3. 自己再给他取个名字
    • Hotspot

      • Oracle收购了Sun,从而得到了Hotspot,又收购了Bin,从而获得了Jrockit(这个东西背整合到了Hotspot中)
      • 还有个ZGC(java11),这个是抄了zing的(算法借鉴嘛)
    • J9

      • IBM自己的
    • LiquidVM

      • Bin公司的
      • 针对于硬件,直接进行操作,快得很
      • 不需要安装操作系统,很快
      • 这个就相当于是一个操作系统,甚至可以不再安装其他的OS
    • Zing

      • 不开源,收费,还贵
      • C4算法:垃圾回收停顿时间控制在1ms
    • TaobaoVM

      • 阿里系的一个Hotspot定制版
    • 毕昇

      • 华为的 ,openJDK的一个定制版
      • 针对IBM的ARM架构进行优化
      • 兼容Java8,但是不能在Windows上跑,只能在Linux或者ARM上一些平台上跑
  • java的运行时数据区:

    • 示意图:

      image-20220216112939155

    • 运行时数据区:经过虚拟化了的,方便找对象

      • 定义:Java虚拟机在执行Java程序时,将内存划分为若干个不同的区域

      • 类型:

        • 线程私有

          • 虚拟机栈
          • 本地方法栈
          • 程序计数器
        • 线程共享:堆,大多数空间分配都在堆上

          • 方法区

            • 运行时常量池
    • 方法区:永久代(JDK 1.7),元空间(JDK 1.8)都是Hotspot的,使用这两个实现了方法区

      • 在Java虚拟机规范中明确提到了方法区的概念;
      • 在Hotspot中以永久代(JDK1.7),元空间(JDK1.8)实现了方法区;因为Hotspot应用广泛,所以这两个概念,广为人知,甚至一度成为方法区的代名词
  • 直接内存:C/C++中使用较多

    • 独立于运行时数据区
    • 未经过虚拟化的,也叫堆外内存;JVM在管理内存的时候将其虚拟化了,new出一个对象,拿到引用就可以操作了

    • 案例:假如一个机器有8G内存,运行时数据区就占了5G,还剩3G。这3G内存,在Java中就可以通过某种方式直接进行操作,使用

      • 但是,这样使用起来会很不方便(C++就很喜欢这个弄)

        • 分配内存

        • 分配地址

        • 数据格式化

  • Java方法的运行与虚拟机栈:

    • 虚拟机栈:存储当前线程运行Java方法所需的数据,指令,返回地址,一块内存(栈)

      • 大小限制:

        • XSS,这个是可以设置的,默认值取决去平台

          • Windows/Linux默认值:1024KB

          • 修改XSS:

            • -Xss 1m 设置为一兆
            • -Xss 1024 不显示指明单位时,默认为比特
            • 不能使用代码进行动态调整的,设置好后,是多大就是多大
        • 也可以查到

        • 一般是1024KB(JDK1.8),这是JVM根据不同的OS与机器的具体情况设置的,它会推荐一个默认的容量

      • 查看虚拟机栈信息

        1. 编写Java程序
        2. 命令打开JDK/lib 键入命令:
      • 启动线程(运行Java程序):

        • 创建一个对应的虚拟机栈

        • 每一个Java方法都对应着一个虚拟机栈

        • Java实质上就是方法调方法

        • 运行方法->创建一个栈帧(放在栈顶,构成了虚拟机栈,它是有一定大小的),若有方法内嵌,创建新的栈帧,每个栈帧是独立的,线程私有的

        • 死递归:虚拟机爆栈 StackOverFlowError,栈溢出一般都是循环

        • 当方法执行完后,栈帧出栈

           public static main (String [] args){
               public void A(){
                   A;
               }
           }
          
        • 代码示例

           public static main (String [] args){
               public void A(){
                   ……
               }
                public void B(){
                   ……
               }
                public void C(){
                   ……
               }
           }
          

          虚拟机栈:栈帧

          image-20210903000753941

      • 栈帧里面到底有什么?

        • 局部变量表,操作数栈,动态链接,完成出口

        • 示意图:Java方法运行的内存区域

          image-20220216114019084

      • 程序计数器:指向当前正在执行的字节码指令的地址(行号,这个要反汇编才能看得到的)

        • 很小的一段内存单位,每个线程都有一个咩?

        • 确保多线程的正常实现,保证JVM的正常运行,保证CPU可抢占且程序无异步现象(当执行到字节码文件的第X行时,调用其他的方法)

        • count:当前字节码文件的行号(有些指令可能会占据多行,所以count值不连续)

        • count:可能出现重复的值

          • 程序计数器是线程私有的,那么每个方法都会对应一个栈帧,当方法执行完毕后,对应的栈帧就会出栈,在调用新的方法时,又会创建新的栈帧,程序计数器又会从零开始,所以难免会出现,count值重复现象
    • 优秀博客:cloud.tencent.com/developer/article/135540

  • 栈帧执行对于内存区域的影响:

    • 示意图:

      image-20220216114143554

    • 源代码

      image-20210903002704452

  • 字节码文件:就是一个Class文件,JVM处理的对象

    • 怎么样去找到这个字节码文件?

      • 可以在编译后:

        1. 打开文件资源管理器,进入out/production/ref-jvm3/原程序包名,采用命令行打开
        2. 输入javp -V Person.class就有了
    • 针对于work() 方法,行号就是字节码文件的地址,实质上是一个偏移量(拿给程序计数器),后面那个就是助记符。 image-20210903002734773
    • 源程序详细流程:

      1. 运行程序,开启线程
      2. 执行main方法,为其创建一个main方法栈帧,并将其压入虚拟机栈
      1. 当源程序执行到第8行时,调用work,触发虚拟机栈,开启新的线程,创建一个新的栈帧

      2. 源程序执行到第9行:

        • 一行java代码(int x =1):对应两行字节码

        • 对应字节码文件中的第0行,此时程序计数器的count=0,

          iconst_1表示new出了一个值为1的int常量,并将这个常量1,压入操作数栈,此时该数位于栈顶

          • iconst_X:将常量X压入操作数栈
          • 这个X是有大小限制的
        • 对应字节码文件中的第1行,此时程序计数器的count=1,

          istore_1(存储命令)表示将操作数栈栈顶的数据放到局部变量表中下标为1的地方

          • 在局部变量表(创建栈帧的时候就有这个表了)中,起点为第0行,默认值为this

            • 为什么是this?

              因为在调用的时候,如果是静态方法,那么这个地方就不要this,这个this只是指明了本方法的调用者是谁(是当前对象);如果说这个地方调用的是静态方法的话,就不需要this了,因为在这个类中,静态方法是跟类挂钩的,与具体的对象没有什么关系

          • 存储命令(操作数栈--->局部变量表):istore_X(将操作数栈栈顶元素压入局部变量中下标为X的位置)

      • 源程序执行到第10行:

        • 原理类似,对应字节码文件中的第2,3行
      • 当源程序执行到第11行(int z = (x+y)*10; ):对应字节码文件中的4,5,6,7,9,10,下对字节码文件进行分析

        • 第4,5行(iload_1与iload_2):将局部表量表中下标为1,2的数据加载到操作数栈中(因为这两个值已经有了,不用再new了)此时程序计数器会跟着变;

          • 加载命令:iload_X(将局部变量表中下标为X的数据压入操作数栈)
          • 此时操作数栈中两个数据,并且2在1的上面
        • 第6行(iadd):将操作数栈中的两个数据出栈(从操作数栈中取出,顺序为2,1),并相加,将得到的结果3重新压入操作数栈,且位于栈顶;

          • 算术指令(iadd):运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并将结果重新存入操作数栈顶

          此时,原来的两个操作数1,2就被扔掉了,那么扔到哪里去了?

        • 第7行(bipush 10):将常量10,推入操作数栈

          • 注意:这里是字节码指令 bipush 10,这是一条指令且占了两个地址空间,偏移量为2;实际上程序计数器记录的是字节码指令的偏移量;

          • 字节码的行号是针对于本方法的偏移

          • 对于int类型:

            • icount_x:x只能在-1,0,1,2,3,4,5中选
            • 其他的int就需要用bipush_x,进行push(凡是用了数据就要压入操作数栈)
        • 第8行丢失:bipush占了两行

          • 底层指令直接操作内存,针对于这个work() 方法,有些指令占据的空间就大
        • 第9行(imul):将操作数栈的元素取出(顺序:10,3),并相乘,将结果重新压入操作数栈,此时操作数栈仅剩栈顶的30

        • 第10行(istore_3):取出操作数栈栈顶元素并存入局部变量表下标为3的位置

        • 第11(iload_3):将局部变量表中下标为3的元素取出,重新压入操作数栈

        • 12行(ireturn):再执行 ireturn

          • 方法的调用与数据类型无关,但是方法的返回指令根据返回值类型进行区分ireturn
          • 将操作数栈的数据返回到main中去:执行栈帧之间的返回(栈帧之间是有嵌套的)
        • 完成出口是什么?

          • 当main中调用work()方法,假设此时字节码的行号为3,那么这个3就记录在完成出口中;当work()方法执行完后,程序接着从main方法的字节码行号为3的地方继续执行
        • 动态链接是什么?

          • 跟多态有关;
        • 程序计数器中的数据可能重复,

          • 因为方法之间会存在嵌套关系,那么栈帧之间也会存在嵌套关系,并且程序计数器中记录的是针对本方法的字节码偏移量;当内嵌方法执行完后,程序跳回上层方法,此时程序计数器中保存的从内嵌--->上层;就有可能出现重复;

          • 但是这个是不影响的,因为:虚拟机栈同一时刻只会执行一个栈帧(就是最顶上的栈帧),执行引擎在执行代码的时候只会找最顶上的栈帧;有了新的方法调用那就压入新的栈帧就行了

          • 程序计数器:确保JVM中单/多线程的正常执行,

            • java中是不能自主控制线程的
            • 当CPU切出去了,没有程序计数器记录当前执行到哪里了,那么CPU切回来的时候就又回重来;
            • 记录每一个字节码执行的地址,也就是状态
    • 操作数栈:栈帧在方法运行完了,这块内存就没有了

    • 关于本地方法

      • 在java虚拟机规范中指出:java虚拟机可能会使用传统的栈来支持native方法(指使用java以外的其他语言编写的方法)的执行,那么这个栈就是本地方法栈
      • Java中是不能直接操作线程的,使用本地方法(操作系统提供的库,可能是汇编等非JVM语系的代码)

      • 本地方法栈结构与虚拟机栈基本类似

      • 示例:public native int hashCode();

        • native:这个就是本地方法,不是Java里面执行的
        • 此时将栈帧压入到本地方法栈栈顶
        • 此时程序计数器无法工作(程序计数器只能记录虚拟机栈,不能记录本地方法栈),count显示为空:因为本地方法中都不是这种字节码的形式了,始终为null
      • 在Hotspot虚拟机中,本地方法栈与虚拟机栈使用同一块内存,不区分这个两个,调的时候看你调的什么方法(native方法就是调本地方法栈):只要合乎Java虚拟机规范就行了,对于具体细节不做深究

    • Java中的类加载机制

      • 源代码:

        image-20220216133751477

      • ObjectAndClass类:放到方法区,类加载的时候放在方法区,

      • 静态变量,常量,静态代码块,这些在类加载的时候会一并放到方法区

      • final static ObjectAndClass lobject = new ObjectAndClass ();12行的那个(改一下)

        • 这段代码在类加载的时候不会执行
      • private boolean isKing;

        • 成员变量在类加载的时候也不会管,它是跟随这个对象的
      • 运行main方法:开启线程

        • int x = 18;与long y =1;局部变量是放在栈帧里面的(操作数栈或者局部变量表,具体要结合实际情况)
      • 在18行:ObjectAndClass object = new ObjectAndClass ();

        • new出一个对象:在堆中进行分配内存,所以说这个new ObjectAndClass)() 在堆

        • 注意这行代码的object是局部变量(new 出的这个对象(在堆中))的引用

          • 这个object是放在虚拟机栈中的局部变量表里面,并且指向堆中new出来的那个内存
        • 注意:当执行完18行以后;那么就要对我new 出的那个对象调用构造方法初始化(完整类的成员变量的初始化),因为在类的成员变量里面还有一句ObjectAndClass lobject = new ObjectAndClass ();

          那么,此时就会再次在堆中分配一个对象,并将其引用lobject 放到堆中的之前分配的那个对象里面去并且指向新分配的对象,这个引用lobject 不会放到虚拟机栈的局部变量表,因为他是之前new出来的 那个对象的类属性:object.lobject (就是这种关系)

      • 直接内存:最后一行

         ByteBuffer bb = ByteBuffer.allocateDirect(128*1021*1024)
        
        • 直接内存可以不释放,JVM中会不断轮询,Union引用实现内存回收

        • 直接内存在源码中在Unsafe类中处理(需要反射修改权限才能玩)

          • 在java9的时候打算去掉但是没有(大量的框架都用了)

          • Unsafe类:

            • 直接操作对象
            • 直接分配内存(绕过JVM,快,但是忘了释放导致内存泄漏,玩多线程可能造成内存覆盖),直接取地址,直接设置地址,直接 设置内存,直接cpoy内存,拿到方法区
      • 注意:在此时new出一个对象后会触发构造方法,再次回到12行,再次创建一个对象,再次触发构造方法,再次回到12行,就死循环了;可以在12行前面加上public static解决这个问题?还是有点疑问
    • 运行时数据区的其他区域

      • 本地方法栈

        • 为JVM使用到本地(Native)方法服务
      • 方法区

        • 永久代与元空间
        • 运行时常量池
      • 直接内存(堆外内存)

        • 没有经过虚拟化

        • 可以使用Unsafe 类进行操作

          • 直接操作对象
          • 释放内存
          • 查看方法偏移量,方法域
          • 找到 .class在哪里
          • CAS操作
          • park操作:阻塞线程
          • unpark操作:唤醒线程
          • 内存屏障
        • Unsafe绕过了JVM的垃圾回收,相当于是一个手动的方式

          • 优点:快,但是用ZGC就差不多了
          • 缺点:忘记释放内存导致内存泄漏,在处理多线程问题的时候极易造成覆盖
  • 深入理解JVM内存区域:

    • 代码执行时JVM的处理流程

      1. JVM申请内存:

        • 方法区:放Class,静态常量
      2. 初始化运行时数据区

      3. 类加载

        • 将Class,静态变,常量放到方法区
      4. 执行方法:

        • 运行一个方法main

          • 创建虚拟机栈

            • main方法里面放一个栈帧

              • 放T1,T2(这两个只是引用,跟普通的变量基本没有区别)指向堆中的对象
              • 只能说现在基本上都是32位指针,到JDK11,强制使用64位指针,此时一个引用就会占据局部变量表中两个局部变量
      5. 创建对象

        • 当引用不再存在,堆中的对象被回收
  • 从底层深入理解运行时数据区:就是对真实地址进行虚拟化

    • 代码展示:

       public class JVMObject {
           public final static String MAN_TYPE = "man"; // 常量
           public static String WOMAN_TYPE = "woman";  // 静态变量
           public static void  main(String[] args)throws Exception {
               Teacher T1 = new Teacher();//这个对象在哪里??哪个地址
               T1.setName("Mark");
               T1.setSexType(MAN_TYPE);
               T1.setAge(36);
               for(int i =0 ;i<15 ;i++){
                   System.gc();//主动触发GC 垃圾回收 15次--- T1存活(因为仍有引用指向这个对象)  T1要进入老年代
               }
               Teacher T2 = new Teacher();
               T2.setName("King");
               T2.setSexType(MAN_TYPE);
               T2.setAge(18);
               Thread.sleep(Integer.MAX_VALUE);//线程休眠   T2还是在新生代
           }
       }
       ​
       class Teacher{
           String name;
           String sexType;
           int age;
       ​
           public String getName() {
               return name;
           }
           public void setName(String name) {
               this.name = name;
           }
       ​
           public String getSexType() {
               return sexType;
           }
           public void setSexType(String sexType) {
               this.sexType = sexType;
           }
           public int getAge() {
               return age;
           }
           public void setAge(int age) {
               this.age = age;
           }
       }
      
    • 示意图:

      image-20220216134755427

    • 新生代:蓝色的,老年代,橙色的(经过多次垃圾回收还存活)

    • 特殊命令:

      • java -cp 执行特定的java类,这个是要配合JHSDB来玩的
      • jps:查看具体的进程
    • 使用JHSDB

      1. java - cp:在命令行执行,要有一个jar包

      2. jps:找出现在虚拟机中正在运行的java程序的进程号

      3. 附着:将这个进程号穿进去

      4. 开始用了

        • 可以去看堆空间的具体划分:这三个区域是紧凑,贴着的,可以看到具体的01代码,来划分

          • Eden
          • From
          • To
    • 内存溢出:

      • 当程序实际使用的空间大于,虚拟机参数(Xms),抛出异常

        • Metaspace:方法区溢出
        • heap space:堆空间溢出
        • stack overflow:栈溢出
        • Direct buffer memory:直接分配内存,导致溢出
  • 问题

    • 动态链接:

      • 基于多态,运行的时候才知道这个方法在哪里。
      • 通过晚期绑定,使用的其他类的方法和变量在发生变化时,不会对调用他们的方法造成影响
    • 操作数栈是属于执行引擎的一部分:有虚拟化

    • 为什么是15次?

      • 对象经历一次垃圾回收,没有被回收,age+1
      • age达到15晋级老年代,因为年龄就是四位二进制记录的
    • 构造方法运行时是栈帧吗?

      • 就是
    • 一个程序所产生的字节码文件,会在程序跑起来后,一次性加载还是按照需求来?

      • 尽可能按照需求
      • 内部有机制的
    • 方法执行完,栈帧消失,那么操作数栈的东西是怎么返回到上一层?

      • 操作数栈不会消失,这个是执行引擎的一部分
      • 这个类似寄存器,所有的数据都会进入操作数栈,操作数栈可以复用
      • 执行引擎 栈(类似告诉缓存) 堆空间
    • 所有类会一次性加载到方法区吗?

      • 按照需求来
    • Unsafe类的是真实地址吗?

      • 操作系统给出的01地址,可能存在映射;
    • 方法没有返回值,那么还有完成出口吗?

      • 有啊,只是说你返回上一层后,不会携带数据
    • JVM处理的 .class 文件到底是什么?

    • 类是静态资源,放到方法区

    • 栈帧里面有什么?

      • 局部变量表

        • 根据程序,保存操作数栈的某些元素
        • 第一行一般为this,费静态方法
      • 操作数栈

        • 只要处理数据,那么数据就要压入操作数栈
        • iload_3:将局部变量表中下标为3的元素压入操作数栈
      • 动态链接

        • 跟多态相关
      • 完成出口

        • 记录跳出方法的地址(有点问题)
        • 比如说,在执行main的过程中,当执行到main对应的第3行字节码时,执行Person person=new Person ();//当执行完成后,回到main的栈帧,接着从第三行字节码执行下去;
  • 编程经验

    • 内存优化:

      • 问题描述:只有200m的内存,现在有500个方法需要运行(一般情况下,一个方法对应一个大小为1m的栈帧)
      • 处理(调整栈帧大小):-Xss 256kb
      • 一般情况下,循环几百,上千次才会出现爆栈
    • 反汇编(有点问题):

      • 找到.class文件,
      • 进入字节码文件,javap -Person.class
    • 在IDEA中如何查找 .class文件

      • out/production/ref-jvm/ex

\