面试官:小伙子,你给我讲一下java类加载机制和内存模型吧 发布文章 ### 类加载机制 虚拟

类加载机制

虚拟机把描述类的数据从 Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。java

类的生命周期

加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)程序员

类加载的过程

类的加载过程包括了加载,验证,准备,解析,初始化
类的加载主要分为如下三步:面试

1. 加载:根据路径找到对应的.class文件

这一步会使用到类加载器。
加载是类加载的一个阶段,注意不要混淆。算法

加载过程完成如下三件事:编程

经过类的彻底限定名称获取定义该类的二进制字节流。
将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
在内存中生成一个表明该类的 Class对象,做为方法区中该类各类数据的访问入口。api

2. 链接:

验证:检查待加载的class正确性;
准备:给类的静态变量分配空间,此时静态变量仍是零值(还没到初始化的阶段)
解析:将常量池的符号引用转为直接引用
符号引用:
符号引用是用一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能够无歧义的定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到内存中。
直接引用:
直接引用能够是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一符号引用在不一样虚拟机实例上翻译出来的直接引用一半不会相同,若是有了直接引用,那引用目标一定已经在内存中存在。
注意:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一块儿被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在全部实例化操做以前,而且类加载只进行一次,实例化能够进行屡次。数组

3. 初始化:对静态变量和静态代码块执行初始化工做

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员经过程序制定的主观计划去初始化类变量和其它资源。缓存

总结

在Java中,类装载器把一个类装入Java虚拟机中,要通过三个步骤来完成:加载、链接和初始化,其中连接又能够分红校验、准备和解析三 步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工做以下:安全

装载:查找和导入类或接口的二进制数据;
连接:执行下面的校验、准备和解析步骤,其中解析步骤是能够选择的;
校验:检查导入类或接口的二进制数据的正确性;
准备:给类的静态变量分配并初始化存储空间;
解析:将符号引用转成直接引用
初始化:激活类的静态变量的初始化Java代码和静态Java代码块网络

类初始化的时机

建立类的实例。new,反射,反序列化
使用某类的类方法–静态方法
访问某类的类变量,或赋值类变量
反射建立某类或接口的Class对象。Class.forName(“Hello”);—注意:loadClass调用ClassLoader.loadClass(name,false)方法,没有link,天然没有initialize
初始化某类的子类
直接使用java.exe来运行某个主类。即cmd java 程序会先初始化该类。

类的加载器(ClassLoader)

类加载器虽然只用于实现类的加载动做,可是还起到判别两个类是否相同的做用。
对于任何一个类,都须要由加载它的类加载器和这个类自己一同确立其在java虚拟机中的惟一性。
一个java程序由若干个.class文件组成,当程序在运行时,会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不一样的class文件中。

程序在启动时,并不会一次性加载程序所要用的全部class文件,而是根据程序的须要,经过java的类加载机制来动态加载某个.class文件到内存当中,从而只有class文件被载入到了内存以后,才能被其余class引用,因此类的加载器就是用来动态加载class文件到内存当中用的。

类加载器如何判断是一样的类

java中一个类用 全限定类名标识——包名+类名
jvm中一个类用其 全限定类名+加载器标识——包名+类名+加载器名

类加载器的种类

从虚拟机的角度来分:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分;
另外一种就是全部其余的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,而且所有继承自java.lang.ClassLoader。
从开发者角度来分:
启动(Bootstrap)类加载器:负责将Java_Home/lib下面的类库加载到内存中(好比rt.jar)。因为引导类加载器涉及到虚拟机本地实现细节,开发者没法直接获取到启动类加载器的引用,因此==不容许直接经过引用进行操做。==加载java核心类

扩展(Extension)类加载器:它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者能够直接使用标准扩展类加载器。

应用程序(Application)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。因为这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以通常称为系统类加载器。它负责加载用户类路径(CLASSPATH)中指定的类库。开发者能够直接使用系统类加载器。默认使用

双亲机制

这里的类加载器不是以继承的关系来实现,都是以组合关系复用父类加载器的代码。

定义:
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若是父类加载器能够完成类加载任务,就成功返回;只有父类加载器没法完成此加载任务时,才本身去加载。

使用双亲委派机制好处在于java类随着它的类加载器一块儿具有了一种带有优先级的层次关系。

具体的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,若是没加载到,则把任务转交给Extension ClassLoader试图加载,若是也没加载到,则转交给App ClassLoader 进行加载,若是它也没有加载获得的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。

若是它们都没有加载到这个类时,则抛出ClassNotFoundException异常。不然将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

为何要使用双亲委托这种模型?

双亲委托机制能够避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

考虑到安全因素,咱们试想一下,若是不使用这种委托模式,那咱们就能够随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在很是大的安全隐患,而双亲委托的方式,就能够避免这种状况,由于String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,因此用户自定义的ClassLoader永远也没法加载一个本身写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

JVM在搜索类的时候,又是如何断定两个class是相同的呢

JVM在断定两个class是否相同时,不只要判断两个类名是否相同,并且要判断是否由同一个类加载器实例加载的。只有二者同时知足的状况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,若是被两个不一样的ClassLoader实例所加载,JVM也会认为它们是两个不一样class。

JAVA内存模型JMM

Java虚拟机规范试图定义一种Java内存模型(JMM),来屏蔽掉各类硬件和操做系统的内存访问差别,让Java程序在各类平台上都能达到一致的内存访问效果。内存模型的做用就是控制一个线程的变量,何时对其余线程可见。

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不一样步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,由于后者是线程私有的,不会共享,固然不存在数据竞争问题。

JMM规定了全部的变量都存储在主内存(MainMemory)中。每一个线程还有本身的工做内存(WorkingMemory),线程的工做内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工做内存的拷贝,可是因为它特殊的操做顺序性规定,因此看起来如同直接在主内存中读写访问通常)。不一样的线程之间也没法直接访问对方工做内存中的变量,线程之间值的传递都须要经过主内存来完成。
Java线程之间的通讯由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。

内存间交互操做

java内存模型中定义了如下8种操做来完成,虚拟机实现时必须保证下面说起的每一种操做都是原子的、不可再分的。

lock锁定:做用于主内存的变量。它把一个变量标识为一条线程独占的状态。
unlock解锁:做用于主内存的变量
read读取:做用于主内存的变量,把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用。
load载入:做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做中内存的变量副本中。
use使用:做用于工做内存的变量,它把工做内存中的一个变量的值传递给执行引擎。每当虚拟机遇到一个须要使用该变量的值的字节码指令时会执行这个操做。
assign赋值:做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
store存储:做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存中,以便随后的write操做使用。
write:做用于主内存的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。

volatile原理

变量对线程的可见性,比synchronized性能好
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:

1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。
不能认为,使用了volatile关键字,就认为并发安全。在一些运算中,因为运算并不是原子操做,仍是会出现同步的问题。

2)禁止进行指令重排序。
  普通变量仅仅会保证在该方法的执行过程当中全部依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操做的顺序与程序代码中的执行顺序一致。
  volatile修饰后,会加入内存屏障(指重排序时不能把后面的指令重排序到内存屏障以前的位置)。执行“lock addl $0x0,(%esp)”,这个操做是一个空操做,做用是使得本cpude Cache写入了内存,该写入动做也会引发别的cpu或别的内核无效化其cache,这种操做至关于对cache中的变量store 和write操做,使得对volatile变量的修改对其余cpu当即可见。

内部原理

处理器为了提升处理速度,不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其余)后再进行操做,但操做完以后不知道什么时候会写到内存,若是对声明了Volatile变量进行写操做,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算操做就会有问题,因此在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里。

volatile关键字

被volatile修饰的共享变量,就具备了如下两点特性:

保证了不一样线程对该变量操做的内存可见性;
禁止指令重排序;

JMM三大特性

原子性

原子性即一个操做或一系列是不可中断的。即便是在多个线程的状况下,操做一旦开始,就不会被其余线程干扰。

好比,对于一个静态变量int x两条线程同时对其赋值,线程A赋值为1,而线程B赋值为2,无论线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操做是没有干扰的,这就是原子性操做,不可被中断的。

由jmm来直接保证的原子性变量操做包括read,load,assign,use,store,write,咱们大体能够认为基本数据类型的访问读写是具有原子性的,(double和long例外)。此外,synchronized块之间的代码也具备原子性

可见性

可见性指的是,当一个线程修改了共享变量的值后,其余线程可以当即得知这个修改。volatile变量、synchronized,final三个关键字修饰的变量均可保证原子性。

有序性

在Java内存模型中有序性可概括为这样一句话:若是在本线程内观察,全部操做都是有序的,若是在一个线程中观察另外一个线程,全部操做都是无序的。

有序性是指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,由于在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。所以,上面概括的前半句指的是线程内保证串行语义执行,后半句则指“指令重排”现象和“工做内存与主内存同步延迟”现象。

java语言提供了volatile和synchronized两个关键字来保证线程之间操做的有序性,volatile关键字自己就包含了禁止指令重排的语义,而synchronized则是由一个变量在同一时刻只容许一条线程对其进行lock操做这条规则得到的。

指令重排

CPU和编译器为了提高程序执行的效率,会按照必定的规则容许进行指令优化。但代码逻辑之间是存在必定的前后顺序,并发执行时按照不一样的执行逻辑会获得不一样的结果。

volatile关键词修饰的变量,会禁止指令重排的操做,从而在必定程度上避免了多线程中的问题

volatile不能保证原子性,它只是对单个volatile变量的读/写具备原子性,可是对于相似i++这样的复合操做就没法保证了。

刚提到synchronized,能说说它们之间的区别吗

volatile本质是在告诉JVM当前变量在寄存器(工做内存)中的值是不肯定的,须要从主存中读取;synchronized则是锁定当前变量,只有当前线程能够访问该变量,其余线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可使用在变量、方法和类级别的;
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则能够保证变量的修改可见性和原子性;
volatile不会形成线程的阻塞;synchronized可能会形成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化。

ABA问题

好比说线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,可是不表明这个过程就是没有问题的。若是链表的头在变化了两次后恢复了原值,可是不表明链表就没有变化

要解决"ABA问题",咱们须要增长一个版本号,在更新变量值的时候不该该只更新一个变量值,而应该更新两个值,分别是变量值和版本号

原子变量

原子变量不使用锁或其余同步机制来保护对其值的并发访问。全部操做都是基于CAS原子操做的。他保证了多线程在同一时间操做一个原子变量而不会产生数据不一致的错误,而且他的性能优于使用同步机制保护的普通变量,譬如说在多线程环境 中统计次数就可使用原子变量。

多线程的使用场景

有时候使用多线程并非为了提升效率,而是使得CPU可以同时处理多个事件。

为了避免阻塞主线程,启动其余线程来作好事的事情,好比APP中耗时操做都不在UI中作.
实现更快的应用程序,即主线程专门监听用户请求,子线程用来处理用户请求,以得到大的吞吐量.感受这种状况下,多线程的效率未必高。 这种状况下的多线程是为了避免必等待,能够并行处理多条数据。好比JavaWeb的就是主线程专门监听用户的HTTP请求,而后启动子线程去处理用户的HTTP请求。
某种虽然优先级很低的服务,可是却要不定时去作。
好比Jvm的垃圾回收。
某种任务,虽然耗时,可是不耗CPU的操做时,开启多个线程,效率会有显著提升。
好比读取文件,而后处理。磁盘IO是个很耗费时间,可是不耗CPU计算的工做。 因此能够一个线程读取数据,一个线程处理数据。确定比一个线程读取数据,而后处理效率高。由于两个线程的时候充分利用了CPU等待磁盘IO的空闲时间。

最后

欢迎你们关注个人公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。