JVM-读书笔记

作者: 我本小生的随笔 | 来源:发表于2016-12-10 22:07 被阅读0次

    java同步:
    背景:因为这个设备的存储读写速度跟处理器的读写速度差异比较大,所以为了保证效率,会有个高速缓存,高速缓存的读写速度与处理器差不多,这个高速缓存就用来作为存储设备和
    1:lock和synchronized实现:有两个准则,第一个是同一个变量在同一时刻只允许一个线程对其lock;第二个准则是在线程对一个变量进行unlock之前,必须把这个变量刷回住内存。这样就保证了一个线程在操作变量时,别的线程没法将这个变量load到线程的工作区域,而线程操作完成之后,变量已经会刷新到主内存,这个时候其它线程才能lock,再去load的时候肯定是最新的值,不会是过期的,所以能达到自己想要的线程安全
    2:volatile关键字的作用是:修改了变量的值,要马上刷回到主内存中去,而线程在使用变量时必须从主内存中load,这样保证单个简单操作时能比较线程安全
    java运行时数据区域:
    1:程序计数器(PC寄存器):用于存放执行的字节码行号,这个的用途在于多线程模型下,CPU会调度程序的运行,就会有这种情况,A线程执行到A方法的第十个字节指令后,CPU切换到B线程执行B方法,然后再切换会A之后,需要让程序恢复到继续执行第十一个字节码指令,程序计数器就用来记录这个,所以它是线程私有的。
    2:java虚拟机栈:与线程的生命周期保持一致,一个方法对应了虚拟机中的一个栈帧,一个方法的调用对应的就是栈帧出栈入栈的过程,栈帧中存储的是局部变量,操作栈,传入的参数和返回值。局部变量包括int,byte,double之类这种基本变量也包括对象引用(这个两种,一种就是指向对象在堆中的地址,堆中的对象会存储着类在方法区中的指针;另一种是指向的是句柄池中的地址,句柄池中的地址由指向堆和方法区中类的地址)和returnAddress
    3:本地方法栈和2差不多,知识用来用于native的方法
    4:堆:是专卖用来存放对象实例的地方,并且基本所有的对象都在这分配,除了JIT技术和一些乱七八糟的技术导致一丢丢实例不在这。因此是GC进行垃圾收集的主要地方也称作GC堆,再就是因为当前垃圾回收主要是分代进行回收,所以堆里会按代进行分配。java堆会随着虚拟机启动而建立,内存由各个线程共享。
    5:方法区:方法区也是线程共享的,存储着加载的类信息,静态变量,常量和即时编译器编译的代码。近乎是永久代。
    6:运行时常量池:是方法区的一部分,在class文件中除了记录类型的版本、方法、字段、接口等描述信息外,还有一项信息是常量池,常量池里存的是各种字面常量,符号引用,这些信息加载后会放在常量池里。与java文件记录的常量池相比,这个不规定只在编译期产生,因此可以后期运行时候加。
    7:直接内存:这个不是运行时数据区域存储的,用于干的是natvie和java搞得,可以看下nio之后再理解这个
    对象访问:涉及到堆,栈,方法区,例如Object object=new Object();new Object()这个涉及到的是在堆上分配内存,存储来Object类型对象所有实例数据值,然后object的一些类型,比方说父类,实现的接口,对象类型、方法等在方法区记录。object变量作为一个reference在栈上记录
    一直在堆上分配内存的话,会造成oom,这个的话,后边会跟着后边会跟着heap相关的提示
    GC:
    程序计数器,java虚拟机栈,本地栈都是随着线程结束而消亡,所以不用太考虑垃圾回收的问题。栈中的栈帧随着方法调用而出栈入栈,并且在类的结构定了之后,栈的大小也就知道了。所以这几部分的内存都是随着方法结束或者随着线程结束而被自动回收掉。不用考虑垃圾收集的事情
    如何判断对象已死:
    1:引用计数法,有地方引用就加一,引用失效时候减一,当技术为0时候,就给回收了,但是一般商用的都不用这个,因为不好解决循环引用的情况
    2:GC Root:就是通过一系列GC Root对象作为基础,然后以这些对象作为头向下搜索,如果有对象不能跟GC ROOT对象链在一起,就可以回收。作为GC Root的来源是:1栈中引用的对象 2:方法区中的静态变量 3方法区中的常量 4:native方法引用的对象
    引用:java中引用有四种,强引用,软引用,弱引用,虚引用
    垃圾收集算法:
    1:标记-清除法:这个简单理解就是先把需要收集的对象标记出来,然后再清理掉,问题有两个,效率不够高,标记和清理的效率都不高,而是清除之后存活的对象不是挨着的,比较碎片化,导致分配连续大量内存的话 需要再进行垃圾收集
    2:复制算法:这个很简单就是把内存分成两份,A份满了之后,把存货的对象复制到B,然后把A删了即可,然后进行来回的循环即可,问题在于这样会浪费百分之五十内存,而大多数对象生命周期又比较短,所以根据这个,再进行分配内存时候不是百分之五十这么分的,会一个Eden,两个Survivor,比例是8:1,这样每次会用掉E+一个S,满了之后,会复制到另一个S。。。如果S不够用就复制到老年代。现代虚拟机的新年代一半用的是这种算法
    3:标记-整理算法:基本思路跟标记-清除算法类似,差别在于在清理时候,存活的对象会向头部移动,这样保证内存是连续的
    4:分代收集算法:没有啥新思路,就是按照对象的生命周期,分成新年代和老年代,然后根据这两个不同的表现,新年代采用复制算法,老年代采用标记清理或者标记整理算法。
    垃圾收集器:基于上边方法,然后虚拟机实现的具体的垃圾收集器。不每一个都说来,基本的差别主要在于采用的什么算法,适合在什么代用;在垃圾收集时候是单线程还是多线程;在垃圾收集时候是一直停顿将垃圾完全收集结束,用户再开始,还是过程中可以停一会,收集一会
    内存分配和回收策略:
    1:内存优先在eden上分配,eden满了的话会有一次minor GC
    2:大对象直接进入老年代,虚拟机比较怕一群生命周期比较短的大对象,因此尽量避免这种用法
    3:存活周期比较长的对象进入老年代:虚拟机会给每一个对象做一个age的标记,没经过一次minor GC值就会加一,当加到一定阀值后会放到老年代
    4:动态选择放入老年代的算法:如果survivor中的对象某一个年龄段的对象大于一半,那么比他老的和他都放到老年代,不用等到阀值
    当放入老年代的对象大于老年代可以容纳的时候就会调用一次full GC
    Class文件格式:
    java class问价实以8位字节为基础单位的二进制流,前后一致相邻没有间隔,如果字段过长,8个字节装不下,就以高位在前的方式分成多个字节进行存储。根据java虚拟机规范的规定,class文件是以类似C语言结构体的伪结构进行存储。这种伪结构只有两种数据类型,无符号数和表。
    无符号数,属于基本的数据类型,包括u1,u2,u4,u8这些不同的数据类型,用于描述数字,索引,数量值,或者按照UTF-8进行编码构成字符串的值
    表,是以无符号数和表一起组成的复合复杂结构。
    因为class文件格式这种的非结构话定义方式,使得每一位和顺序都是严格定义的
    class具体格式定义:
    1:u4 魔数
    2:u2 最小版本号
    3:u2 主版本号
    4:接下来是常量池,是class出现的第一个表类型数据结构的数据,是与其它项目关联最多的数据,也是最大的数据量结构之一,常量池由于大小不一致所以会在开头记录长度,常量池中主要存着两类数据:字面量和符号引用。字面量比较接近于java层面的常量概念:例如,文本字符串,被声明为final的变量。符号引用是编译层面的概念:1:类或接口的全限定名 2:方法名和描述符 3:字段名和描述符
    4.1: u2 常量池容量计数(从1开始)
    4.2: 常量池中的每一项都是表,第一个字段是u1类型标识符,用于标识数据类型,比方说int;字符串;方法、字段或类的引用,然后每种类型都会有特定的存储方式,具体可以看一下class的文件。
    5: 常量池之后紧跟的是访问修饰符,用于表明是不是public,是不是接口,是不是abstract,是不是final
    6:类索引,父索引,接口索引:类索引,父索引都是u2类型数据,接口索引是u2类型数据集合,由这三项数据可以确定类的继承关系类索引和父索引不用解释了,就挨着,接口索引开始会有一个u2数据来描述数量,然后后边跟着接口索引,
    7:后边跟着的是字段表:第一个字段是u2类型,记录字段的访问修身符,类级字段还是实例级字段、可变性、并发可见性、可否序列化等;第二个字段也是u2类型,用来记录字段的name—index;第三个字段是字段的description_index字段;这两个字段用来记录,字段和方法的名称,和数据类型、返回值、输入参数等;字段表不会列出从父类继承的字段,只会出现java代码中从来没出现过的字段
    8:方法表:和字段表基本一致。编译之后的java代码会放到方法表中属性表里的Code属性中。在class的文件中对方法的签名会包括返回值,所以返回值不同也可以重载,但是在eclipse里会编译不过去,基于的编译器不是JavaC
    9:属性表,属性表是在class文件、字段表、方法表中都可以携带自己属性的集合。
    9.1:Code属性,主要会记录代码的长度(code_length是u4类型,所以方法中最大不能够超过65535个字节码指令),代码的内容;操作栈的最大深度;局部变量所需的空间(实例级别的函数,局部变量从1开始,0默认存着的是this);接下来是异常表,异常其实就是把各种形式编译成不同的分支,有异常走到哪,没一场走到哪。
    。。。。。。。。。。。。
    虚拟机类加载机制:
    java虚拟机类加载和连接都发生在运行期,这样会导致运行期开始有一些性能开销,但是增加了灵活性。加载机制是双亲委托模型,意思是啥呢,如果一个classloader继承了上层的classloader之后呢,当用这个classloader加载一个类时候,会优先去它父类中看看是不是能加载这样依次找上去,找到最顶层的classloader,如果它可以加载就用它加载,如果加载不了,就向下再找,这样个人感觉保障了安全性,使得主要的尤其是系统级的类库加载的是java制定的那些,而不是自己偷偷模仿改的版本.
    Class文件从被加载到虚拟机内存到被卸载为止,总共需要经过加载、验证、准备、解析、初始化、使用、卸载七个生命周期。其中验证、准备、解析可以称作是连接过程。
    虚拟机规范对于加载过程何时执行,没有严格的限制,但是对于合适初始化严格定义了四种情况(加载、验证、准备、解析肯定要在此之前发生)
    1:遇到new,getstatic,putstatic,invoklestatic时候,如果没有初始化类时候需要立刻初始化类,对应的就是实例化一个对象、读取或者设置一个静态字段(final的除外)、调用静态方法
    2:通过反射对类进行反射调用时候
    3:当初始化一个类时候,父类还没有初始化的情况
    4:指定的main方法的类
    加载过程:
    1:通过类的权限定名加载class文件二进制流
    2:将class的二进制的结构转换为方法区的运行时数据结构
    3:在堆中分配类的一个实例,作为方法区数据的访问的入口
    方法区中的数据结构又虚拟机自己进行定义,虚拟机并没有严格规范其定义方式
    验证阶段:
    验证阶段是为了保证虚拟机的安全性,避免class的二进制流文件不合法把虚拟机搞崩了
    1:文件格式验证,魔数,主次版本,常量池的类型是不是有未定意的,索引是否有指向不存在的常量或不匹配的常量,utf-8是否合理
    2:元数据验证,对语意进行验证,包括是否有父对象,是否继承了不允许继承的类,类中的字段是否和父对象的有矛盾,是不是抽象类,有没有未实现的接口或抽象方法
    3:字节码验证:保证任意时刻操作数栈的数据类型与指令码序列都能配合工作,保证跳转指令不会跳转到字节码意外的范围,保证方法中的类型转换时合法的,
    4:符号引用验证:根据类名是否有类,是否有方法和字段,访问性是否可以
    准备阶段:
    准备阶段时为类变量分配内存和设置初始值的过程,内存分配在方法区,初始值是各种值的默认值,对应实际的赋值要在 cinit方法时候执行的,实例变量是在初始化时候在堆上分配的
    解析阶段:
    将虚拟机常量池中的符号引用(一般是用来表征目标的字面量)转换为直接引用(指针或者偏移,句柄之类的)的过程,
    类或接口的解析:主要就是把全限定名交给类加载器去加载
    字段或者方法解析:
    初始化阶段:
    除了类加载使用自定义的类加载器之外,其余的情况都是由虚拟机进行控制,这一过程也是java代码开始执行的地方。
    准备阶段已经将类变量设置为初始值,现在就有初始化阶段对其进行赋值,最终会形成<cinit>方法,收集类类变量,静态语块,他们执行的顺序是在源文件出现的前后顺序,虚拟机保证子类的<cinit>方法调用之前会调用到父类的<cinit>,方法不是必须生成的,如果一个类没有静态变量的话,就不会生成这个方法。虚拟机会保证<cinit>方法的线程安全。
    类加载器:
    使用类加载器的一个潜在的坑或者问题是,如果同一个类的加载器不同,那么这两个类则不相等,这样就会导致instanceof,equals,isinstance之类的方法都失效了
    字节码执行引擎:
    栈帧:是用于支持虚拟机方法调用和方法执行的数据结构,栈帧时在虚拟机栈中的元素,在其中存储了局部变量表、操作数栈、动态链接和返回地址等信息。每一个方法的调用对应的都是进栈出栈的过程。局部变量表大小和操作数栈的深度,在代码编译后已经确定,将会存储在code属性中,
    动态连接:栈帧中都会包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了动态连接。字节码中的方法调用是通过符号索引调用的,这个符号索引有的在类加载阶段变为直接引用,这个称作是静态解析。另一部分在每一次运行时转换为引用,称作是动态连接。
    方法调用:
    方法调用不等同于方法执行,方法调用时确定调用方法的版本。在方法调用时候主要是用来确定方法调用的版本,而不涉及调用方法内部的具体运行过程。
    这给java带来动态扩展的能力,但也增加了java在方法调用过程的复杂度。方法的调用需要再类加载后,甚至是运行期才能确定版本
    解析:
    所有方法调用中的目标方法,都是class文件中的常量池中的一个符号引用,类加载解析阶段会把一部分符号引用转换为直接引用。这部分解析是编译器可知,运行期不变的方法,主要是静态方法,私有方法。
    声明变量方法的变量时静态类型,new出来的变量是动态类型。静态变量是编译期、可知的,动态类型是运行期可知的。在方法调用时,确定重载的定义版本时候,根据的是变量的静态类型(编译器确定),所有依赖静态类型进行定位方法执行版本的分派动作称为是静态分派。静态分派的典型应用就是方法重载。当然对于重载方法的运行时候,静态分派找重载方法的时候,策略是找到最合适的版本。想个例子就是test(1),有int,char,double的版本,肯定选int版本,没有int版本,选char版本,这个没有选double的版本。
    动态分派是实现多态的另一个形式,复写实现的根本。在调用方法时候,其实根据反射的调用,应该知道是需要把对象传进去,在调用方法时候,先看他有没有实现,没有的话找父,这样覆写实现了的话,肯定搞子的实现。
    字节码执行:
    字节码执行是基于栈进行操作,而不是基于地址,简单的概述流程就是,方法在调用的时候,在遇到变量分配的时候,这些变量会放在局部变量表里,并且知道它在局部变量表中的索引,而方法在执行过程中的时候,需要操作这些个临时变量时候,就根据索引把这个索引的局部变量load到操作数栈里边,这样新load的会放在栈顶,完成操作时候,就把栈顶的弹出操作,操作完成后再压入栈顶。
    编译优化,前期编译,JavaC,Eclipse这种;
    JIT,即时编译,这个是在运行期把一些调用频次高的函数,即时编译成本地码运行,提高之后的运行效率;
    AOT:提前编译,在运行之前,预先编译,将字节码编译为本地码,这种预编译使得连接变为静态连接而不是动态连接,这样使java动态扩展的特性丧失了,如果是这样的话,需要考虑art下怎么进行动态扩展,插件话之类的操作。

    相关文章

      网友评论

        本文标题:JVM-读书笔记

        本文链接:https://www.haomeiwen.com/subject/dgpumttx.html