zoukankan      html  css  js  c++  java
  • 学习笔记(每天持续更新)

    计算机网络

    五层TCP/IP协议模型

    物理层

    物理层的任务是确定与传输媒体的接口有关的一些特性。
    物理层是体系结构的最低层,建立在物理通信介质上,但应注意,物理层并不是指传输介质。
    物理层处理数据的单位是二进制位,比特(bit)。
    物理层定义了纯粹的物理及电子技术细节,通过物理连接的建立实现传输介质上比特流的传输。

    数据链路层

    链路层的任务是在两个相邻结点间传送数据帧。
    链路层传送数据的单位是“帧”。
    从网络层接收数据,将特定的控制信息封装到名为“帧”的数据信息里,再将其传给物理层。
    数据链路层能提供差错控制

    网络层

    网络层的任务:①负责为分组交换网上的不同主机提供通信服务。②选择合适的路由。
    网络层传送数据的单位是:包,也叫分组或IP数据报(或数据报)。
    网络层主要使用的协议:
    IP协议--提供无连接的,尽最大努力交付的数据报服务。
    在源和目的结点之间选择路由

    运输层

    运输层的任务是负责向两台主机中进程之间的通信提供通用的数据传输服务。
    运输层是OSI模型中承上启下的层,它下面的3层主要面向网络通信,上面的应用层则面向主机用户
    运输层有复用和分用的功能。
    运输层能确保数据包顺利地到达目的地,保证数据包不丢失、不重复和无差错
    在端到端之间可靠地传输报文。

    应用层

    是体系结构的最高层,其任务是通过应用进程间的交互来完成特定网络应用。
    应用层交互的数据单元:报文
    应用层的功能最丰富,实现也最复杂。应用层的协议很多,如支持万维网的HTTP协议,支持电子邮件的SMTP协议,支持文件传输的FTP协议等等。
    直接为用户的应用进程提供服务

    从输入网址到获得页面的过程

    1. 浏览器查询 DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的DNS缓存、搜索操作系统的DNS缓存、读取本地的Host文件和向本地DNS服务器进行查询等。
    2. 浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;
    3. TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求;
    4. 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
    5. 浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;
    6. 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。

    ARP协议

    在以太网环境中,数据的传输所依懒的是MAC地址而非IP地址,而将已知IP地址转换为MAC地址的工作是由ARP协议来完成的。

    • ARP请求
      任何时候,当主机需要找出这个网络中的另一个主机的物理地址时,它就可以发送一个ARP请求报文,这个报文包好了发送方的MAC地址和IP地址以及接收方的IP地址。因为发送方不知道接收方的物理地址,所以这个查询分组会在网络层中进行广播。
    • ARP响应
      局域网中的每一台主机都会接受并处理这个ARP请求报文,然后进行验证,查看接收方的IP地址是不是自己的地址,只有验证成功的主机才会返回一个ARP响应报文,这个响应报文包含接收方的IP地址和物理地址。这个报文利用收到的ARP请求报文中的请求方物理地址以单播的方式直接发送给ARP请求报文的请求方

    数据结构

    B树(B-树)和B+树

    https://my.oschina.net/u/4116286/blog/3107389

    红黑树

    二叉查找树:
    左子树值均小于根节点,右子树均大于根节点
    左右子树均为二叉排序树(二分查找的思想)

    二叉查找树的缺点:
    不断地插入递增或者递减的值,可能会变成线性表

    即将二叉查找树变成一种平衡树

    红黑树就是一种策略,也就是说红黑树是一种平衡二叉查找树

    红黑树特点:

    1. 节点是红色或者黑色
    2. 根节点是黑色
    3. 每个叶子的节点都是黑色的空节点(NULL)
    4. 每个红色节点的两个子节点都是黑色的。
    5. 从任意节点到其每个叶子的所有路径都包含相同的黑色节点。

    ....变色和旋转,理解不动啊。。。

    数据库

    索引的失效情况

    1. 条件中有or,无论什么索引都会失效
      • 可以用in
      • 使用or,又想索引生效,只能将or条件中的每个列都加上索引
    2. 复合索引中需要遵循最左前缀
      • 比如有(A,B,C)索引,索引生效的情况有A,AB,ABC
      • 如果查询中有AC这样,那么只有A索引条件生效
    3. 范围查询后面的查询,索引不成效
      • A=x,B>x,C=x,C不生效
    4. 在索引上进行运算,索引失效
    5. 字符串没有单引号,索引失效
    6. 尽量使用覆盖索引(只访问索引的查询(索引列完全包含查询列)),减少select * 。查询列,超出索引列,也会降低性能。
    7. 以%开头的Like模糊查询,索引失效。
      • 如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。
    8. in 走索引, not in 索引失效。
    9. 尽量使用复合索引,而少使用单列索引
      • 创建复合索引(A,B,C),相当于创建了A,A+B,A+B+C

    索引的结构,这里说的是B+树索引

    https://blog.csdn.net/weixin_30531261/article/details/79312676

    • 索引为什么不能是链表结构

    假设有name索引,在我们找到第一个name属性为“Jesus”的节点后,继续往后找,当遇到name属性不为“Jesus”的节点时,就无需再往后查找了,因为节点是根据name属性有序排列的啊。假设第一个name=“Jesus”的节点是第499个节点,最后一个name=“Jesus”的节点是第500个节点,那么只需要遍历501个节点就可以了。当发现第501个节点的name字段不为“Jesus”,后面的499个节点也就无需遍历了。

    但是这样的话前500次查询是很慢的,效率太低了。

    • 索引为什么不能是平衡二叉树
      平衡树的查找时间复杂度是O(log2N),已经很不错了

    索引是存在于索引文件中,是存在于磁盘中的。因为索引通常是很大的,因此无法一次将全部索引加载到内存当中,因此每次只能从磁盘中读取一个磁盘页的数据到内存中。而这个磁盘的读取的速度较内存中的读取速度而言是差了好几个级别。

    注意,我们说的平衡二叉树结构,指的是逻辑结构上的平衡二叉树,其物理实现是数组。然后由于在逻辑结构上相近的节点在物理结构上可能会差很远。因此,每次读取的磁盘页的数据中有许多是用不上的。因此,查找过程中要进行许多次的磁盘读取操作。

    而适合作为索引的结构应该是尽可能少的执行磁盘IO操作,因为执行磁盘IO操作非常的耗时。因此,平衡二叉树并不适合作为索引结构。

    • 为什么B树适合做索引
      B树是为了充分利用磁盘预读功能来而创建的一种数据结构
      • 磁盘的局部性原理:
        磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。
        程序运行期间所需要的数据通常比较集中。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。

    B树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点。也正因每个节点存储着非常多个关键字,树的深度就会非常的小。进而要执行的磁盘读取操作次数就会非常少,更多的是在内存中对读取进来的数据进行查找。

    B树的查询,主要发生在内存中,而平衡二叉树的查询,则是发生在磁盘读取中。因此,虽然B树查询查询的次数不比平衡二叉树的次数少,但是相比起磁盘IO速度,内存中比较的耗时就可以忽略不计了。因此,B树更适合作为索引。

    • 更加适合做索引的B+树

    B+树的关键字全部存放在叶子节点中,非叶子节点用来做索引,而叶子节点中有一个指针指向一下个叶子节点。做这个优化的目的是为了提高区间访问的性能。而正是这个特性决定了B+树更适合用来存储外部数据。

    事务的基本要素(ACID)

    https://www.cnblogs.com/wyaokai/p/10921323.html
    https://baijiahao.baidu.com/s?id=1611918898724887602&wfr=spider&for=pc

    1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位
    2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
    3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
    4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

    对于隔离性,有着事务的隔离级别,如下

    事务隔离性的隔离级别

    1. 脏读:事务A操作的数据(比如插入)还没有提交,但是事务B却此时可以读到,读到的数据就是脏数据
    2. 不可重复读: 假设事务A要读某一数据两次或者多次,在事务A读取数据的时候过程中,事务B更改了数据并且提交了,还没有结束的事务A读取到的数据不一致了
    3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

    小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

    MySQL事务隔离级别

    未提交读(Read Uncommitted):允许脏读,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值。也就是可能读取到其他会话中未提交事务修改的数据
    提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。
    可重复读(Repeated Read):可重复读。无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响。
    串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

    JAVA内存模型

    https://zhuanlan.zhihu.com/p/29881777
    https://blog.csdn.net/fu123123fu/article/details/79794017

    什么是java内存模型

    Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

    调用栈和本地变量存放在线程栈上,对象存放在堆上。

    JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。

    对于普通共享变量,线程A将变量修改后,体现在此线程的工作内存。在尚未同步到主内存时,若线程B使用此变量,从主内存中获取到的是修改前的值,便发生了共享变量值的不一致,也就是出现了线程的可见性问题。
    所有的变量都存储在主内存中
    每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
    线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
    不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量的传递需要通过主内存来完成

    线程1对共享变量的修改要想被线程2及时看到,必须经过如下2个步骤:
    1>把工作内存1中更新过的共享变量刷新到主内存中
    2>将主内存中最新的共享变量的值更新到工作内存2中
    java语言层面支持的可见性实现方式有以下两种:
    synchronized
    volatile
    volatile定义:

    当对volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存
    写操作会导致其他线程中的缓存无效
    这样,其他线程使用缓存时,发现本地工作内存中此变量无效,便从主内存中获取,这样获取到的变量便是最新的值,实现了线程的可见性。

    java内存模型存在的问题

    缓存一致性问题 :在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等:

    指令重排序问题 :为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化

    Java内存模型和硬件内存架构之间的桥接

    Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。

    线程之间的共享变量存储在主内存(Main Memory)中
    每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
    从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
    Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

    JMM模型下的线程间通信

    线程间通信必须要经过主内存。

    如下,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:

    1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

    2)线程B到主内存中去读取线程A之前已更新过的共享变量。

    关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

    • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
    • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
    • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
    • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
    • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
    • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
      Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

    如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

    • 不允许read和load、store和write操作之一单独出现
    • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
    • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
    • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
    • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
    • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
    • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
    • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

    Java内存模型解决的问题

    当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。Java内存模型建立所围绕的问题:在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争race condition)。
    当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。Java内存模型建立所围绕的问题:在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争race condition)。

    多线程读同步与可见性

    可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改

    线程缓存导致的可见性问题:

    如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不可见的:共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中,然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

    下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。

    解决这个内存可见性问题你可以使用:

    • Java中的volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

    volatile特性:
    1>能够保证volatile变量的可见性
    2>不能保证volatile变量复合操作的原子性

    如何实现内存可见性?
    深入来说:通过加入内存屏障和禁止重排序优化来实现的
    1>对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
    2>对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
    通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

    线程写volatile变量的过程:
    1.改变线程工作内存中volatile变量副本的值
    2.将改变后的副本的值从工作内存刷新到主内存
    线程读volatile变量的过程:
    1.从主内存中读取volatile变量的最新值到线程的工作内存中
    2.从工作内存中读取volatile变量的副本

    volatile不能加锁,对number++;的操作会被多个线程交叉执行,导致出现不同的结果。
    假设num=5,即使对变量加上了volatile,此时想象为电脑速度放慢了1000倍,在线程A取出变量num,读取到工作内存,在还没有把num+1,并且刷新到主内存时,线程B做得比较快,已经把num+1,读取到并且已经刷新到主内存,此时还会出现错误
    为解决这个问题,可以为num++加上synchronized,也就是上锁

    • Java中的synchronized关键字:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
      上面的话翻译一下的话就是:
      1>线程解锁前(退出synchronized代码块之前),必须把共享变量的最新值刷新到主内存中,也就是说线程退出synchronized代码块值后,主内存中保存的共享变量的值已经是最新的了
      2>线程加锁时(进入synchronized代码块之后),将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)
      两者结合:线程解锁前对共享变量的修改在下次加锁时对其他线程可见
      根据以上推出线程执行互斥代码的过程:
      1>获得互斥锁(进入synchronized代码块)
      2>清空工作内存
      3>从主内存拷贝变量的最新副本到工作内存
      4>执行代码
      5>将更改后的共享变量的值刷新到主内存
      6>释放互斥锁(退出synchronized代码块)

    • Java中的final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)

    volatile适用场所
    要在多线程中安全的使用volatile变量,必须同时满足:
    1.对变量的写入操作不依赖其当前值
    不满足:number++,count=count*5
    满足:Boolean变量,记录温度变化的变量等
    2.该变量没有包含在具有其他变量的不变式中
    不满足:不变式low小于up

    synchronized和volatile的比较
    1.volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
    2.从内存可见性角度讲,volatile读操作=进入synchronized代码块(加锁),volatile写操作=退出synchronized代码块(解锁)
    3.synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,不能保证原子性

    重排序导致的可见性问题:

    Java程序中天然的有序性可以总结为一句话:如果在本地线程内观察,所有操作都是有序的(“线程内表现为串行”(Within-Thread As-If-Serial Semantics));如果在一个线程中观察另一个线程,所有操作都是无序的(“指令重排序”现象和“线程工作内存与主内存同步延迟”现象)。

    Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性:

    volatile关键字本身就包含了禁止指令重排序的语义
    synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

    指令序列的重排序:

    代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化(有些代码翻译成机器指令之后,如果进行一个重排序,那可能重排序之后的顺序更加符合CPU执行的特点,这样就可以最大限度发挥CPU的性能)

    1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

    2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    数据依赖:

    编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑)

    指令重排序对内存可见性的影响:

    总结一句话就是,没有数据依赖的指令可能会被重排,在多线程的时候,由于指令重排会发生错误

    as-if-serial语义:

    无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)
    int num1=1;
    int num2=2;
    int sum=num1+num2;
    单线程:第1、2行的顺序可以重排,但第3行不能

    happens before:

    在JMM中,如果一个操作执行的结果需要对另一个操作可见(两个操作既可以是在一个线程之内,也可以是在不同线程之间),那么这两个操作之间必须要存在happens-before关系:

    • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
    • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
    • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

    一个happens-before规则对应于一个或多个编译器和处理器重排序规则

    内存屏障禁止特定类型的处理器重排序:

    重排序可能会导致多线程程序出现内存可见性问题。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

    多线程原子性

    由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,我们大致可以认为基本数据类型变量、引用类型变量、声明为volatile的任何类型变量的访问读写是具备原子性的(long和double的非原子性协定:对于64位的数据,如long和double,Java内存模型规范允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性,即如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。但由于目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此在编写代码时一般也不需要将用到的long和double变量专门声明为volatile)。这些类型变量的读、写天然具有原子性,但类似于 “基本变量++” / “volatile++” 这种复合操作并没有原子性。
    如果应用场景需要一个更大范围的原子性保证,需要使用同步块技术。Java内存模型提供了lock和unlock操作来满足这种需求。虚拟机提供了字节码指令monitorenter和monitorexist来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步快——synchronized关键字。

    JAVA 多线程

    synchronize

    https://cloud.tencent.com/developer/article/1465413

    synchronize的特性

    1. 原子性
      要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
      对基本数据类型的变量的读取和赋值操作是原子性操作。
      但是像i++、i+=1等操作字符就不是原子性的,它们是分成读取、计算、赋值几步操作
      被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。

    注意!面试时经常会问比较synchronized和volatile,它们俩特性上最大的区别就在于原子性,volatile不具备原子性。
    2. 可见性
    synchronized和volatile都具有可见性
    synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性。
    3. 有序性
    synchronized和volatile都具有有序性
    有序性值程序执行的顺序按照代码先后执行。
    Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
    4. 可重入性
    synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

    synchronize的用法

    1. 加给类的非静态成员方法:此时的这个锁就是加给类所实例化的对象的
    2. 加给类的静态成员方法:此时的这个锁就是加给
    3. 加给xx.class,此时就是加给某一个类的
    4. 加给某一实例,此时就是加给某一个实例化对象的

    我们要进入某一个同步方法或者同步代码块的时候必须要获得相应的锁才可以

    上面的简单意思就是用两个线程分别对i加100万次,理论结果应该是200万,而且我还加了synchronized锁住了add方法,保证了其线程安全性。可是!!!我无论运行多少次都是小于200万的

    因就在于synchronized加锁的函数,这个方法是普通成员方法,那么锁就是加给对象的,但是在创建线程时却new了两个Test2实例,也就是说这个锁是给这两个实例加的锁,并没有达到同步的效果
    同时i++也不是原子操作,所以值不会等于200万

    synchronize的实现

    加给类的成员方法,此时通过反汇编可以发现,多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,知道该锁被释放。

    如果是同步代码块,代码如下

    从反编译的同步代码块可以看到同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。

    但是为什么会有两个monitorexit呢?其实第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行goto指令,而该指令转向的就是23行的return,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

    synchronize锁的底层实现

    首先,实例化的对象是分成三部分实现的
    对象头,实例数据,对其填充

    实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。

    对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,

    • Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,
    • Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。

    锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

    每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。

    JVM

    Minor Gc 和 Full GC 有什么不同呢?

    目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
    新生代 GC(Minor GC): 指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
    老年代 GC(Major GC/Full GC): 指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

    什么对象会进入老年代

    大对象直接进入老年代
    大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
    长期存活的对象将进入老年代
    既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
    如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1. 对象在 Survivor 中每熬过一次 MinorGC, 年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。

    对象的引用的分类(四种)

    1. 强引用
      以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

    2. 软引用(SoftReference)
      如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
      软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

    3. 弱引用(WeakReference)
      如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
      弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    4. 虚引用(PhantomReference)
      “虚引用” 顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
      虚引用主要用来跟踪对象被垃圾回收的活动。

    5. 虚引用与软引用和弱引用的一个区别:
      虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
      特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

    如何判断一个类是无用的类

    方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
    判定一个常量是否是 “废弃常量” 比较简单,而要判定一个类是否是 “无用的类” 的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

    1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    2. 加载该类的 ClassLoader 已经被回收。
    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是 “可以”,而并不是和对象一样不使用了就会必然被回收。

    如何判断一个常量是废弃常量

    运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
    假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。
    JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

    如何判断对象是否死亡(两种方法)

    1. 引用计数法
      给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
      这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,假如除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

    2. 可达性分析算法
      这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

    注意:不可达的对象并非 “非死不可”

    即使在可达性分析法中不可达的对象,也并非是 “非死不可” 的,这时候它们暂时处于 “缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;

    1. 可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
    2. 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

    垃圾回收算法

    1.标记-清除算法 Mark-Sweep
    这是一个非常基本的GC算法,它是现代GC算法的思想基础,分为标记和清除两个阶段:先把所有活动的对象标记出来,然后把没有被标记的对象统一清除掉。但是它有两个问题,一是效率问题,两个过程的效率都不高。二是空间问题,清除之后会产生大量不连续的内存。

    2.复制算法 Copying (新生代)
    复制算法是将原有的内存空间分成两块,每次只使用其中的一块。在GC时,将正在使用的内存块中的存活对象复制到未使用的那一块中,然后清除正在使用的内存块中的所有对象,并交换两块内存的角色,完成一次垃圾回收。它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。它的致命缺点是会有一半的内存浪费。

    3.标记整理算法 Mark-Compact(老年代)
    标记整理算法适用于存活对象较多的场合,它的标记阶段和标记-清除算法中的一样。整理阶段是将所有存活的对象压缩到内存的一端,之后清理边界外所有的空间。它的效率也不高。
    区别
    (1)效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
    (2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。
    (3)内存利用率:标记/整理算法=标记/清除算法>复制算法。

    4.分代收集算法:(新生代的GC+老年代的GC)
    少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。

    大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。

    垃圾回收器

    1.并行和并发概念补充
    (1)并行(Parallel)
    指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    (2)并发(Concurrent)
    指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

    2.Serial 收集器
    Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
    新生代采用复制算法,老年代采用标记 - 整理算法。

    3.ParNew 收集器
    ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
    新生代采用复制算法,老年代采用标记 - 整理算法。

    4.Parallel Scavenge 收集器
    Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。
    Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

    5.Serial Old 收集器
    Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

    6.Parallel Old 收集器
    Parallel Scavenge 收集器的老年代版本。使用多线程和 “标记 - 整理” 算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

    7.CMS 收集器
    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
    CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
    从名字中的 Mark Sweep 这两个词可以看出,CMS 收集器是一种 “标记 - 清除” 算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
    (1)初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
    (2)并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    (3)重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
    (4)并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。
    CMS收集器是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

    • 对 CPU 资源敏感;
    • 无法处理浮动垃圾;
    • 它使用的回收算法 -“标记 - 清除” 算法会导致收集结束时会有大量空间碎片产生。
    1. G1 收集器
      G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
      被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
      并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
      分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
      空间整合:与 CMS 的 “标记 – 清理” 算法不同,G1 从整体来看是基于 “标记整理” 算法实现的收集器;从局部上来看是基于 “复制” 算法实现的。
      可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
      G1 收集器的运作大致分为以下几个步骤:
    • 初始标记
    • 并发标记
    • 最终标记
    • 筛选回收
      G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region (这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)

    类加载机制

    类加载的过程
    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:


    其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
    下面就一个一个去分析一下这几个过程。

    1、加载
    ”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
    (1)通过一个类的全限定名来获取其定义的二进制字节流
    (2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
    (3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
    相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了。
    2、验证
    验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
    (1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。
    (2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
    (3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。
    (4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
    对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。
    3、准备
    准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
    (1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
    (2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如
    public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
    当然还有其他的默认值。

    注意,在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。
    4、解析
    解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
    符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
    5、初始化
    这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。
    在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
    ①声明类变量是指定初始值
    ②使用静态代码块为类变量指定初始值
    JVM初始化步骤
    1、假如这个类还没有被加载和连接,则程序先加载并连接该类
    2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
    3、假如类中有初始化语句,则系统依次执行这些初始化语句
    类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
    创建类的实例,也就是new的方式访问某个类或接口的静态变量,或者对该静态变量赋值调用类的静态方法反射(如 Class.forName(“com.shengsiyuan.Test”))初始化某个类的子类,则其父类也会被初始化Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类好了,到目前为止就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器。在加载阶段其实我们提到过类加载器,说是在后面详细说,在这就好好地介绍一下类加载器。

    类加载器

    虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。
    1、Java语言系统自带有三个类加载器:
    Bootstrap ClassLoader :最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。Extention ClassLoader :扩展的类加载器,加载目录%JRE_HOME%libext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类。我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。这三种类加载器的加载顺序是什么呢?
    Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader
    一张图来看一下他们的层次关系

    代码验证一下:

    从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
    2、类加载的三种方式
    认识了这三种类加载器,接下来我们看看类加载的三种方式。
    (1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
    (2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
    (3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
    下面代码来演示一下
    首先我们定义一个FDD类
    public class FDD {static { System.out.println("我是静态代码块。。。。"); }}
    然后我们看一下如何去加载
    package com.fdd.test;public class FDDloaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader loader = HelloWorld.class.getClassLoader(); System.out.println(loader);//一、 使用ClassLoader.loadClass()来加载类,不会执行初始化块loader.loadClass("Fdd"); //二、 使用Class.forName()来加载类,默认会执行初始化块 Class.forName("Fdd"); //三、使用Class.forName()来加载类,指定ClassLoader,初始化时不执行静态块 Class.forName("Fdd", false, loader); } }
    上面是同不同的方式去加载类,结果是不一样的。
    3、双亲委派原则
    他的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)
    采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:
    可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。4、自定义类加载器
    在这一部分第一小节中,我们提到了java系统为我们提供的三种类加载器,还给出了他们的层次关系图,最下面就是自定义类加载器,那么我们如何自己定义类加载器呢?这主要有两种方式
    (1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
    (2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
    我们看一下实现步骤
    (1)创建一个类继承ClassLoader抽象类
    (2)重写findClass()方法
    (3)在findClass()方法中调用defineClass()

    个人qq:835493858 有事联系我
  • 相关阅读:
    使用 SQL Server 2008 数据类型-xml 字段类型参数进行数据的批量选取或删除数据
    启用Windows 7/2008 R2 XPS Viewer
    Office 2010培训资料
    WCF WebHttp Services in .NET 4
    ASP.NET MVC 2示例Tailspin Travel
    .NET 4.0 的Web Form和EF的例子 Employee Info Starter Kit (v4.0.0)
    连任 2010 年度 Microsoft MVP
    MIX 10 Session下载
    Microsoft Silverlight Analytics Framework
    Windows Azure入门教学
  • 原文地址:https://www.cnblogs.com/wpbing/p/14422042.html
Copyright © 2011-2022 走看看