Skip to content

Latest commit

 

History

History
281 lines (101 loc) · 11 KB

JAVA核心知识.md

File metadata and controls

281 lines (101 loc) · 11 KB

[toc]

JAVA核心知识

JVM工作原理

​ 将字节码转换为机器编码

​ 解释执行和即时编译执行----一般是混合执行----codecache可能会满

​ 垃圾回收 安全检查等

  • jvm 一行代码是怎么运行的?

    ​ 首先,java代码会被编译成字节码,字节码就是java虚拟机定义的一种编码格式,需要java虚拟机才能够解析,java虚拟机需要将字节码转换成机器码才能在cpu上执行。

    ​ 我们可以用硬件实现虚拟机,这样虽然可以提高效率但是就没有了一次编译到处运行的特性了,所以一般在各个平台上用软件来实现,目前的虚拟机还提供了一套运行环境来进行垃圾回收,数组越界检查,权限校验等。

    ​ 虚拟机一般将一行字节码解释成机器码然后执行,称为解释执行,也可以将一个方法内的所有字节码解释成机器码之后在执行,前者执行效率低,后者会导致启动时间慢,一般根据二八法则,将百分之20的热点代码进行即时编译。JIT编译的机器码存放在一个叫codecache的地方,这块内存属于堆外内存,如果这块内存不够了,那么JIT编译器将不再进行即时编译,可能导致程序运行变慢。

类加载器

​ 加载---双亲委派、获取字节码,类加载器决定一个类的唯一性 (类加载器+类名)

​ 链接---验证、分配内存、解析

​ 初始化---cinit()方法加synchronized

  • jvm如何加载一个类

    第一步:加载,双亲委派:启动类加载器(jre/lib),系统扩展类加载器(ext/lib),应用类加载器(classpath),前者为c++编写,所以系统加载器的parent为空,后面两个类加载器都是通过启动类加载器加载完成后才能使用。

    ​ 加载的过程就是查找字节流,可以通过网络,也可以自己在代码生成,也可以来源一个jar包。另外,同一个类,被不同的类加载器加载,那么他们将不是同一个类,java中通过类加载器和类的名称来界定唯一,所以我们可以在一个应用程序存在多个同名的类的不同实现。

    第二步:链接:(验证,准备,解析) 验证主要是校验字节码是否符合约束条件,一般在字节码注入的时候关注的比较多。准备:给静态字段分配内存,但是不会初始化,解析主要是为了将符号引用转换为实际引用,可能会触发方法中引用的类的加载。

    第三步:初始化,如果赋值的静态变量是基础类型或者字符串并且是final的话,该字段将被标记为常量池字段,另外静态变量的赋值和静态代码块,将被放在一个叫cinit的方法内被执行,为了保证cinit方法只会被执行一次,这个方法会加锁,我们一般实现单例模式的时候为保证线程安全,会利用类的初始化上的锁。 初始化只有在特定条件下才会被触发,例如new 一个对象,反射被调用,静态方法被调用等

一个对象的内存结构

  • 对象头加上字段

  • 字段重排序

  • 独占缓存行 falseSharding

  • 计算对象占用大小

  • Java对象的内存布局

​ java中每一个非基本类型的对象,都会有一个对象头,对象头中有(16字节,前八个字节markword)64位作为标记字段,存储对象的哈希码,gc信息,锁信息,另外64位存储class对象的引用指针,如果开启指针压缩的话,该指针只需要占用32位字节。

  • 为什么是64位,工业界得出的结论最优, cpu在进行读取的时候,遵循的是按块读取,(程序运行原理:时间局部性/空间局部性),(即执行完当前指令后会很快执行下次或者挨着的空间的时间),充分发挥cpu针脚读取数据能力,提高效率

Java对象中的字段,会进行重排序,主要为了保证内存对齐,使其占用的空间正好是8的倍数,不足8的倍数会进行填充,所以想知道一个属性相对对象其始地址的偏移量需要通过unsafe里的fieldOffset方法,

内存对齐也为了避免让一个属性存放在两个缓存行中,disruptor中为了保证一个缓存行只能被一个属性占用,也会用空对象进行填充,因为如果和其他对象公用一个缓存行,其他对象的失效会将整个缓存行失效,影响性能开销,jdk8中引入contended注解来让一个属性独占一个缓存行,将注解属性移动到远离对象头的地方,处于不同缓存行,内部也是进行填充,用空间换取时间,

  • 如何计算一个对象占用多少内存?

如果不精确的话就进行遍历然后加上对象头,这种情况没办法考虑重排序和填充,如果精确的话只能通过javaagent的instrument工具。

  • 创建一个对象的过程

    1:检查类是否已经被加载;

    ​ 当程序遇到new关键字的时候,首先会去运行时常量池检查该引用所指向的类是否被JVM加载,如果么有被加载,会进行类的加载过程,如果已经被加载,那么进行下一步

    2:为对象分配内存空间;

    ​ 需要在堆内存中为该对象分配一定空间,空间的大小在类加载完成的时候就已经确定下来啦.

    ​ 其中为对象分配空间方式有两种,

    ​ a. 指针碰撞(Bump the pointer) jvm将堆区抽象为两个区域,一个已经被占用,一个是空白区域,中间通过一个指针进行标注,这时只需要将指针向空白区域移动相应大小空间,就完成了内存分配,当然这种方式需要jvm的堆内存地址连续,且堆内存带有内存压缩机制,可以在分配完成时候压缩内存,形成连续的地址空间.这种分配方式成为"指针碰撞",但明显这种方式是有问题的,当多线程时候,划分内存会出现不一致的情况.当A线程刚将指针移到新位置,B线程读取了之前的指针地址,会出现. JVM对这样的情况采用了CAS配上失败重试的方式保证更新操作的原子性.

    ​ b.**空闲列表(Free List)**第二种也是微了第一种分配方式的不足而创建.

    ​ 多线程分配内存时候,虚拟机为每个线程分配了不同的空间,这样每个线程再分配内存时候,只在自己的空间中操作,从而避免了上述问题,不需要同步,但是当线程空间用完了后,需要申请空间,这时候需要进行同步锁. 这种方式成为"本地线程分配缓冲空间(TLAB)"是否启用TLAB需要通过 -XX:+/-UseTLAB参数来设定

    3:为对象字段设置零值;

    ​ 分配完内存后需要对对象字段进行初始化 null 0 "" 等

    4:设置对象头;

    ​ jvm对创建出来的对象进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息等,这些标记存放在对象信息头中

    5:执行构造方法。

    ​ 初始化对象。

    源码到汇编

    0 new #2

    3 dup

    4 invokespecial #3 <T.>

    7 astore_1

    8 return

    eg:

    public class T(){

    ​ int m = 8;

    ​ }

    T t = new T();

    ​ 0 t------------------------------->开辟一个内存空间

    ​ 4 对对象字段 int m=0; 根据属性类型赋默认值

    ​ 7 进行关系绑定,即设置对象信息头,标记新生代、老年代、哈希码,元数据信息

    初始化对象,执行构造方法 int m = 8

  • 对应问题 DCL(Double check Lock)单例是否需要volatile

    ​ CPU执行创建对象命令的时候,是会乱序执行的,(在这个时候,半初始化的时候)命令交换了,直接建立关联,可能就会导致新来的线程调用有问题。

    ​ 因为volatile修饰的对象,不可以乱序执行,就能保证不会被拿到半初始化状态的对象

    volatile 怎么保证不乱序呢, 通过内存屏障 jvm级别的屏障是一种规范不是具体实现,不是os级别、硬件级别的实现

    写内存屏障(Store Memory Barrier):处理器将存储缓存值写回主存(阻塞方式)。

    读内存屏障(Load Memory Barrier):处理器,处理失效队列(阻塞方式)。

    保证两个操作之间数据的可见性。

    volatile读前插读屏障,写后加写屏障,避免CPU重排导致的问题,实现多线程之间数据的可见性

​ 对于处理器来说,内存屏障会导致cpu缓存的刷新,刷新时,会遵循缓存一致性协议。

​ lock**:解锁时,jvm会强制刷新cpu缓存,导致当前线程更改,对其他线程可见。

​ volatile**:标记volatile的字段,在操作时,会强制刷新cpu缓存,标记volatile的字段,每次读取都是直接读内存

final:即时编译器在final写操作后,会插入内存屏障,来禁止重排序,保证可见性

java对象内存模型

  • ​ 并发的本质 原子性、可见性、乱序执行 (乱序执行可以进行反证法)
  • ​ happen-before描述内存可见性 a happen-before b a线程的所有操作对线程b是可见的

管程:

文档:线程共享数据可见性.note 我的note有道的笔记 链接:http://note.youdao.com/noteshare?id=fe2e3867cfe1799c399da8fedf94770e

Java虚拟机内存结构

image-20200923144406453

各个区域的功能不是本文重点,就不在这里详细介绍了。这里简单提几个需要特别注意的点:

1、以上是Java虚拟机规范,不同的虚拟机实现会各有不同,但是一般会遵守规范。

2、规范中定义的方法区,只是一种概念上的区域,并说明了其应该具有什么功能。但是并没有规定这个区域到底应该处于何处。所以,对于不同的虚拟机实现来说,是有一定的自由度的。

3、不同版本的方法区所处位置不同,上图中划分的是逻辑区域,并不是绝对意义上的物理区域。因为某些版本的JDK中方法区其实是在堆中实现的。

4、运行时常量池用于存放编译期生成的各种字面量和符号引用。但是,Java语言并不要求常量只有在编译期才能产生。比如在运行期,String.intern也会把新的常量放入池中

5、除了以上介绍的JVM运行时内存外,还有一块内存区域可供使用,那就是直接内存。Java虚拟机规范并没有定义这块内存区域,所以他并不由JVM管理,是利用本地方法库直接在堆外申请的内存区域。

6、堆和栈的数据划分也不是绝对的,如HotSpot的JIT会针对对象分配做相应的优化。

如上,做个总结,JVM内存结构,由Java虚拟机规范定义。描述的是Java程序执行过程中,由JVM管理的不同数据区域。各个区域有其特定的功能。

垃圾回收算法

反射机制

动态代理

IO

并发工具包

线程

锁和Synchronized

集合

面向对象设计原则

设计模式