Java虚拟机学习笔记三

时间:2022-06-01 20:06:06
26、在加载(Loading)阶段,虚拟机要完成一下3件事情:
    1):通过一个类的全限名来获取此类的二进制字节流。
    2):将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3):在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    
27、类加载过程中,可以分为非数组类和数组类的加载过程。
    非数组类:加载阶段可以使用系统提供的引导类加载器完成,也可以有用户自定义加载器去完成(重写一个类加载器的loadClass方法)
    数组类:
        1):如果组建是引用类型,那就递归加载的过程去加载组件类型(父类到子类),数组c将在加载该组件类型的类加载器的类名称空间被标识。
        2):如果是普通类型如int[],则会把数组c标记为引导类加载器关联。
   加载完后,虚拟机外部的二进制字节流按照虚拟机所需格式存储在方法区之中。对hotspot虚拟机而言,Class对象比较特殊,虽然是对象,但是是存放在方法区里面。

28、验证:保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。
    1):文件格式验证,例如包括是否以魔数0xCAFEBABE开头、主次版本是否在当前虚拟机处理范围之内。
    2):元数据验证,保证其描述信息符合java语言规范,例如有木有继承多个类,有木有继承final类,实现接口是否实现所有方法。
    3):字节码验证,通过数据流和控制流分析,确定语义是合法的、符合逻辑的。
    4):符号引用验证,这个阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段-解析阶段发生。
        例如:符号引用中通过字符串描述的全限名称是否能找到对应的类。
        
   对虚拟机类加载机智类说,验证阶段是一个非常重要,但不是一定必要的阶段,如果所需的类都被反复使用和验证过,就可以使用-X verify:none参数关闭大部分的类验证措施。

29、准备:正式为类变量分配内存并设置类初始值的阶段,这些类使用的内存都是在方法区中分配。注意:这里指的类变量是static修饰的变量,而不包括实例变量。
   注意,是初始值。
    例如:
    public static int value = 123;
    经过这一步后,初始值为0,而不是123,而把value值赋值为123的putstatic指令是程序被编译后,存放于类构造器clinit方法之中,所以复制将在初始化阶段才执行。
    如果是:
    public static final int value = 123;
    则在这一步,就会将value赋值为123.

30、解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
   主要针对类,接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限,首先都是解析符号引用。
    1):类或接口的解析:
        #如果不是一个数组,就把全限名传递给类加载器去加载
        #是一个数组,且数组类型为对象,就需要去加载数组元素类型,接着由虚拟机生成一个代表数组唯独和元素的数组对象。
        #如果已经成为了一个有效的类和接口,就只需要确认是否有该类或接口的访问权限,没有就会抛出java.lang.IllegalAccessError异常。
    2):字段解析:
        直接本类找或者通过递归向上找。
    3):类方法解析;
    4):接口方法解析;

31、初始化:类初始化阶段是类加载过程的最后一部,前面除了用户可以自定义加载器参与之外,其余动作完全由虚拟机主导控制,到初始化阶段,才是真正的开始执行类
   中定义的java程序代码了。
    #<clinit>()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句快中语句合并产生的,顺序有源文件顺序。
    #<clinit>()方法与类的构造方法不同,他不需要调用父类构造函数,虚拟机会保证子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
    因此在虚拟机中第一个被执行的<clinit>()方法肯定是java.lang.Object的。
    #如果没有静态代码块和复制操作,就不会有这个<clinit>()方法。
    #接口虽然不能使用静态语句快,但可能有赋值操作,因此也会有<clinit>()方法,但执行接口的<clinit>()方法时,不需要执行父接口的<clinit>()方法,
    另外,在接口实现类初始化中,也不需要执行<clinit>()方法。
    #虚拟机会保证一个类的<clinit>()方法在多线程中线程安全,进行正确的加锁,同步,等。

32、#虚拟机的设计团队把类加载器中的“通过一个类的全限名来获取描述次类的二进制字节流”,把这个动作放到虚拟机外实现,以便让应用程序能自己决定如何去获取所需类,
   实现这个动作的代码模块称为类加载器。
   #每一个类加载器都拥有一个独立的类名称空间,都有一个即使两个类来源与同一个class,被同一个虚拟机加载,只要加载的类加载器不同,那这两个类比不相等。

33、类加载器分类:
    1):启动加载器(Bootstrap ClassLoader):负责实例化话lib目录下
    2):扩展类加载器,负责加载/lib/ext目录等。
    3):应用程序加载器,加载用户类路径下指定类库,也成为系统类加载器。

34、类加载器的双亲委派模型(Parents Delegation Model):要求除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器,并且一般不是以继承而是以
   组合的方式实现。工作过程如下:如果一个类加载器收到了加载的请求,他首先不会尝试去加载这个类,而是把请求委托给父类加载器完成,只有当父类加载器无法完成时,
   子类才会尝试自己加载。所以所有的加载请求最终都应该传送到顶层的启动类加载器中。
   具有层次关系,并且保证java体系中最基础的行为。
   保证唯一性。

35、栈帧的结构:包括了局部变量表、操作数栈、动态连接、方法返回地址等。
    1):局部变量表:用于存放方法参数和方法内部定义的局部变量。
    2):操作数栈:先入后出,一开始为空,方法执行中进行入栈出栈操作。
    3):动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,目的是支持方法调用过程中的动态连接。
    4):方法返回地址:方法退出时候记录地址,一般是pc计数器的值。

36、方法调用:不等同于方法的执行,唯一的任务就是确定被调用方法版本,即调用哪一个方法。
    1):调用目标在程序代码写好,编译器进行编译时就必须确定下来,这类方法的调用成为解析。
       2):只要能被invokestatic和invokespecial指令调用的,都可以在解析阶段确定唯一调用版本,符合条件的有静态方法、私有方法、实例构造器、父类方
    法,这些成为非虚方法。
       而其他的则成为虚方法,final方法不是虚方法(虽然final方法是有invokevirtual来调用的)。
    3):解析调用是一个静态的过程,而分派调用则可能是静态也可能是动态。
    4):气势静态分派就是编译时多胎,重载,而动态分派则是运行时多态,重写

37、执行方法时候的类型匹配是按照char->int->long->float->double来进行的,只会向上自动进行,而不会向下自动进行。
   如果有两个方法参数有相同的优先级,则编译器无法确定要自动转型为哪一种类型,就会提示类型模糊,拒绝编译。

38、什么是动态类型语言?动态类型语言的关键特征是它的类型检查主题过程是在运行期而不是编译期。例如js,php,python等,变量无类型而变量值有类型。
   而在编译期就进行类型检查的语言如c++和java就是静态类型语言。

39、invokedynamic指令与前面的4条“invoke*”指令的最大差别就是,它的分派逻辑不是由虚拟机决定的,而是由程序员决定的。

40、基于栈的指令集有点就是可移植。就是执行起来稍微慢一点。

41、一段简单代码的反汇编,即字节码表示:
    源代码:

    public static void main(String[] args){
        int a = 100;
        int b = 200;
        int c = 300;
        System.out.println((a + b)*c);    
        }
    执行javap -c name.class后得到:
    public static void main(java.lang.String[]);
        Code:
           0: bipush        100                  //将一个int压入栈,即a入栈
           2: istore_1                //将a保存
           3: sipush        200            //接着又是一个int压入栈。
           6: istore_2
           7: sipush        300
          10: istore_3
          11: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
          14: iload_1                //取出a
          15: iload_2                //取出b
          16: iadd                    //计算a+b并压入
          17: iload_3                //取出c
          18: imul                    //计算(a+b)*c并压入
          19: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
          22: return
    
    如上可知,
    需要深度为2的操作数栈,和4个Slot的局部变量空间。

42、tomcat有4个目录可以存放类库,
    #/common:类库可被tomcat和所有web应用程序共同使用
    #/server:只能被tomcat使用
    #/shared:可被所有web应用程序共同使用,但对tomcat不可见
    #/webapp/WEB-INF/:只对当前web应用可见

43、tomcat的webapp类加载器和jsp类加载器可能存在多个实例,每一个web应用程序对应一个webapp类加载器,每个jsp对应一个jsp类加载器。

44、编译期是一个不确定的操作过程,可能指以下三个方面:
    #前端编译器,把java文件转变成class文件过程。
    #JIT编译器,把字节码转变成机器码的过程。
    #AOT编译器,Ahead Of Time Compile,直接把java文件转变成本地机器代码过程。

45、虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这里面的优化过程对程序运行来说更重要,而前端编译器(javac)的优化过程对于程序编码来说关系更加密切。

46、前端编译器,大致分为3个过程:
    1):解析与符号填充
        #词法分析、语法分析
        #填充符号表
    2):插入式注解处理过程
    3):分析与字节码生成过程
        #标注检查
        #数据及控制流分析
        #解语法糖,如利用语法糖来实现泛型
        #字节码生成

47、在java中,List<Integer>和List<String>在编译后都被擦除了,编程一样的原生类型List<E>,而c#则不会进行擦除,而是两个不同的类。

48、JRockit里面只有编译器,而不是编译器与解释器共存。
   Hotspot虚拟机内置了两个即时编译器,分别是Client Compiler和Server Compiler,或者简称为C1,C2.

49、运行期优化步骤:这些代码是建立在代码的某种中间表示或机器码之上的,绝不是建立在java源码之上的。
    1):方法内连,通过方法内连可以去掉方法调用成本(建立帧),为其他优化建立良好基础。
    2):消除冗余控制
    3):复写传播,即消除多余变量。
    4):无用代码清除。

50、java内存模型:
    1):主内存与工作内存。
        #java内存模型规定所有变量都存储在主内存,每条线程还有自己的工作内存。
        #如果局部变量是一个reference类型,它引用的对象在java堆中可被各个线程共享,但reference本身在java栈的局部变量表中,是私有的。
    2):内存间的交互操作。
        #jmm中定义了一下8中操作来完成,必须保证每一种操作都是原子的不可再分的:
        lock,unlock,read,load,use,assign,store,write.
    3):8中基本操作必须满足如下规则:
        #不允许read和load,store和write操作之一单独出现。
        #不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步到主内存。
        #不允许一个线程无原因的(没发生过assign操作)把数据从工作内存同步回主内存
        #新变量只能在主内存中诞生,即对一个变量实施use和store操作之前,必须执行过assign和load。
        #一个变量一次只能允许一条线程进行lock操作,但lock能被执行多次,多次lock后,只有被执行同样多次unlock才能解锁。
        #如果对一个变量进行lock操作,那将会清空工作内存中此变量的值,需要重新执行load和assign操作初始化变量的值
        #lock和unlock搭配使用
        #对一个变量执行unlock之前,必须把此变量同步回主内存。
    3):volatile变量:一个是可见性,修改后的新值对其他线程是立即可见的。只能保证可见性,而不能保证线程安全。另一个是禁止指令重排序优化。
        #volatile在并发时候,同样是会出现不安全的问题,因为java里面的运算是非原子操作的。
        因此还是需要加锁来保证原子性。
        #volatile变量读操作的性能小号与普通变量几乎没有什么差别,但是写操作可能会慢一些。
    4):对于long和double型变量的特殊规则
        #允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位操作,不保证64位数据的原子性。
    5):原子性、可见性、有序性
        #对于可见性而言,普通变量和volatile变量读取都是要先刷新主内存后读取,指示volatile变量的特殊规则保证新值能够立即同步到主内存。
        除了volatile之外,还有synchronized和final也能实现可见性,对于    
            #同步块,对于一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
            #final,被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this引用传递出去,那在其他线程中就能看到final字段的值。
        #对于有序性,如果在本线程内观察,所有操作都是有序的,而在一个线程观察另一个线程,所有操作都是无序的。java提供volatile和synchronize
        来保证线程之间操作的有序性,synchronize解决了线程之间的顺序问题。
    6):先行发生原则,
        #程序次序规则
        #管程锁定规则,unlock先与同一个锁的lock操作
        #volatile变量规则,对volatile变量的写操作优先于读操作
        #线程启动规则,start()方法优先与此线程每一个动作
        #线程终止规则,所有操作都先于终止
        #线程中断规则,对线程的interrupt()方法的调用先行于被中断线程代码检测到中断事件的发生。,也就是中断了以后,你就别去interrupt了
        #对象终结规则,构造函数执行结束先于finalize方法。
        #传递性,a先于b,b先于c,所以a先于c。

51、java中线程状态转化:
    #新建new
    #运行run
    #无限期等待waiting
        #没有设置timeout的wait()方法
        #没有设置timeout的join()方法
        #LockSupport.park()方法
    #限期等待
        #Thread.sleep()方法
        #设置了timeout的wait方法
        #设置了timeout的join方法
        #LockSupport.parkNanos()方法
        #LockSupport.parkUntil()方法
    #阻塞,和等待不同,阻塞是获取了cpu,但是在等待获取一个排他锁
    #结束

52、java语言中各种操作共享的数据分为一下5类:
    #不可变
    #绝对线程安全
    #相对线程安全
    #线程兼容
    #线程对立

53、线程安全的实现方法
    #互斥同步
    #非阻塞同步
    #无同步方案

54、synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码序列,这两个字节码都需要一个reference类型的参数来指明
   要锁定和解锁的对象。monitorenter将锁的计数器加1,monitorexit将锁的计数器减1.
   synchronized同步块对于同一个线程来说是可重入的,不会出现自己把自己锁死的状况。

55、CAS指令需要有3个操作数,分别是内存位置v,旧的预期值a,新的预期值b,CAS指令执行时,当切仅当v的值为a时,才会去将v的值更新为b,但是无论是否更新了v的值,
   最终都是返回v的旧值。
   CAS操作会有ABA问题,例如一个变量v初次读取的时候是A,并且在准备赋值的时候判断它仍然是A,那么就能说明它没有被其他线程改了么?不,有可能先被改为了B,
   然后又改回A,那CAS操作就会误认为它从来没有被改变过,这个漏洞就是ABA问题。大部分情况下,ABA问题并不会影响到并发的正确性,如果需要解决ABA问题,那该用传统
   互斥同步可能会比原子类更高效。

56、锁优化
    #自旋锁和自适应锁
    #锁消除
    #锁粗化
    #轻量级锁。
    #偏向锁:消除无竞争下的同步原语,进一步提高同步性能,偏向于第一个获取它的线程,如果该所没有被其他线程获取,则持有偏向锁的线程将永远不需要同步。
    当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。
    如果说轻量级锁是在无竞争情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争情况下把整个同步块消除吊,连CAS都不做了。