全网最全!这份深刻讲解jdk和jvm原理的笔记,刷新了我对JVM的认知

前言

前两天和朋友探讨技术的时候有聊到JVM和JDK这一块,聊到这里两我的就像高山流水遇知音那是根本停不下来,过后我想着趁如今印象还比较深入就把这些东西整理起来分享给你们来帮助更多的人吧。话很少说,满满的干货都整理在下面了!前端

JVM探究

jvm的位置

jvm的体系结构java

堆里面有垃圾,须要被GC回收程序员

栈里面是没有垃圾的,用完就弹出去了,栈里面有垃圾,程序就崩了,执行不完main方法。面试

Java栈,本地方法栈,程序计数器里面是不可能存在垃圾的。也就不会有垃圾回收。算法

所谓的jvm调优就是在堆里面调优了,jvm调优99%都是在方法区和堆里面进行调优的。编程

类加载器

public class Car {
    public static void main(String[] args) {
        Car car1 = new Car();
        Car car2 = new Car();
        Car car3 = new Car();
        System.out.println(car1.hashCode());
        System.out.println(car2.hashCode());
        System.out.println(car3.hashCode());
        Class<? extends Car> aClass1 = car1.getClass();
        Class<? extends Car> aClass2 = car2.getClass();
        Class<? extends Car> aClass3 = car3.getClass();
        System.out.println(aClass1.hashCode());
        System.out.println(aClass2.hashCode());
        System.out.println(aClass3.hashCode());

    }
}

做用:加载class文件 - 相似new Student();后端

类是一个模板,是抽象的,而new出来的对象是具体的,是对这个抽象的类的实例化数组

1.虚拟机自带的加载器缓存

2.启动类(根)加载器安全

3.扩展加载器

4.应用程序(系统类)加载器

ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);//AppClassLoader 应用程序加载器

System.out.println(classLoader.getParent());//ExtClassLoader 扩展类加载器

System.out.println(classLoader.getParent().getParent());//null 1.不存在  2.Java程序获取不到

1.类加载器收到类加载的请求

2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到根加载器

3.启动类加载器会检查是否可以加载当前这个类,能加载就结束,使用当前加载器,不然,抛出异常,通知子类加载器进行加载。

4.重复步骤3

若都找不到就会报 Class Not Found

null:Java调用不到,可能编程语言是C写的,因此调不到

Java =C+±- 去掉C里面比较繁琐的东西 指针,内存管理(JVM帮你作了)

双亲委派机制

双亲委派机制:安全

APP–>EXC–BOOTStrap(根目录,最终执行)

当某个类加载器须要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操做,若是上级的类加载器没有加载,本身才会去加载这个类。

在src下建立Java.lang包,建立一个String类

package java.lang;

public class String {
    public String toString(){
        return "hello";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.getClass().getClassLoader());
        s.toString();
    }
}

执行结果

它会去最终的BOOTStrap里面的String类里面去执行,找到执行类的位置,发现里面没有要执行的mian方法,因此会报这个错。

在src下建立类Student

public class Student {
    public String toString(){
        return "HELLO";
    }

    public static void main(String[] args) {
        Student student = new Student();
        System.out.println(student.getClass().getClassLoader());
        System.out.println(student.toString());
    }
    
}

执行结果

如上图可见最终是在APP里面执行的,成功输出HELLO语句

双亲委派机制的做用

一、防止重复加载同一个.class。经过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
二、保证核心.class不能被篡改。经过委托方式,不会去篡改核心.class,即便篡改也不会去加载,即便加载也不会是同一个.class对象了。不一样的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

沙箱安全机制

​ Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,而且严格限制代码对本地系统资源访问,经过这样的措施来保证对代码的有效隔离,防止对本地系统形成破坏,沙箱主要限制系统资源访问,那系统资源包括什么?CPU,内存,文件系统,网格,不一样级别的沙箱对这些资源访问的限制也是能够不同。

​ 因此的Java程序运行均可以指定沙箱,能够定制安全策略。

​ 在Java中将执行程序分为本地代码呵远程代码两种,本地代码默认视为可信任的,而远程代码则被看做是不受信任的,对于受权的本地代码,能够访问一切本地资源,而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制,以下图jdk1.0安全模型

但如此严格的安全机制也给程序的功能扩展带来障碍,好比当用户但愿远程代码访问本地系统的文件时候,就没法实现,所以在后续的Java1.1版本中,针对安全机制作了改进,增长了安全策略,容许用户指定代码本地资源的访问权限,以下图所示JDK1.1安全模型

​ 在Java1.2版本中,再次改进了安全机制,增长了代码签名,不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不一样的运行空间,来实现差别化的代码执行权限控制,以下图所JDK1.2安全模型

当前最新的安全机制实现,则引入域(Domain)的概念,虚拟机会把全部代码加载到不一样的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则经过系统域的部分代理来各类需求的资源进行访问,虚拟机中不一样的受保护域,对应不同的权限,存在不一样域中的类文件就具备了当前域的所有权限,以下图所示,最新的安全模型

组成沙箱的基本组件:

字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范,这样能够帮助Java程序实现内存保护,但并非全部的类文件都会通过字节码校验,好比核心类。

类加载器(class loader):其中类加载器在3个方面对Java沙箱起做用

​ 它防止恶意代码去干涉善意的代码;//双亲委派机制

​ 它守护了被信任的类库边界;

​ 它将代码纳入保护域,肯定了代码能够进行哪些操做;

​ 虚拟机为不一样的类加载器载入的类提供不一样的命名空间,命名空间由一系列惟一的名称组成,每个被装载的类将有一个名字,这个命名空间由Java虚拟机为每个类加载器维护的,它们互相甚至不可见。

​ 类加载器采用的机制是双亲委派模式。

​ 虚拟机为不一样的类加载开始加载,外层恶意同名类得不到加载从而没法使用;

​ 因为严格经过包来区分了访问域:外层恶意的类经过内置代码也没法得到权限访问到内置类,破坏代码就天然没法生效。

存取控制器(access controller):存取控制器能够控制核心API对操做系统的存取权限,而这个控制的策略设定,能够由用户指定。

​ 安全管理器 (security manager):是核心API和操做系统之间的主要接口,实现权限控制,比存取控制器优先级高。

​ 安全软件包(security package):java.security下的类和扩展包下的类,容许用户为本身的应用增长新的安全特性,包括:

​ 安全提供者

​ 信息摘要

​ 数字签名 kettools https(须要证书)

​ 加密

​ 鉴别

Native

凡是带了native关键字,说明Java的的做用范围达不到了,会去调用底层C语言的库!

会进入本地方法栈

调用本地方法本地接口 JNI

JNI的做用:扩展Java的使用,融合不一样的编程语言为Java所用,最初:C,C++

Java诞生的时候C ,C++比较火,Java想要立足,必需要有调用C,C++的程序。

他在内存区域专门开辟了一块标记区域 :Native Method Stack,登记native方法

在最终执行的时候,加载本地方法库中的方法经过JNI

Java程序驱动打印机,管理系统,掌握便可,在企业级应用中较为少见。

​ 目前该方法使用使用的愈来愈少了,除非 是与硬件有关的应用,好比经过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见,由于如今的异构领域间通讯很发达,好比可使用Socjet通讯,也可使Web Service等等 ,很少作介绍!

PC寄存器

​ 程序计数器:Program Counter Register

​ 每一个线程都有一个程序计数器,要线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个很是小的内存空间,几乎能够忽略不计。

方法区

Method Area 方法区

​ 方法区是被全部线程共享,全部字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,全部定义的方法的信息都保存在该区域,在此区域属于共享区间

​ 静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,可是实例变量存在堆内存中,与方法区无关。

static,final,Class,常量池

栈是一种数据结构

​ 程序 = 数据结构 + 算法 :持续学习~

​ 程序 = 框架 + 业务逻辑 :吃饭~

栈:先进后出,后进先出

队列:先进先出(FIFO:First input First Output)

方法运行完成之后,就会被栈弹出去

两个方法互相调,就会致使栈溢出

public class Inn {
    public static void main(String[] args) {
        new Inn().test();
    }
    public void test(){
        a();
    }
    public void a(){
        test();
    }
    //a调test,test调a
}

运行结果

栈:栈内存,主管程序的运行,生命周期和线程同步,也就是线程若是都结束了,栈也就变成空的了;

线程结束,占内存也就释放了,对于栈来讲,不存在垃圾回收问题,一旦线程结束,栈就没了。

栈:8大基本类型+对象引用+实例的方法

栈运行原理:栈帧

函数调用过程当中,确定须要空间的开辟,而调用这个函数时为该函数开辟的空间就叫作该函数的栈帧

程序正在执行的方法必定在栈的顶部,执行完就会弹出去

栈1在运行完成以后就会弹出去,而后栈2在去执行,栈2执行完,程序也就结束了

栈满了:StackOverflowError

栈+堆+方法区 :交互

以下图:对象在内存中实例化的过程

三种jvm

Sun 公司 HotSpot Java HotSpot™ 64-Bit Server VM (build 25.77-b03, mixed mode)
BEA :JRockit
IBM :J9 VM
咱们用的是hotspot


Heap(堆):一个jvm只有一个,堆内存的大小是能够调节的。

类加载器读取了类文件后,通常会把什么东西放到堆中呢?类,方法,常量,保存咱们全部引用类型的真实对象。

堆内存中还要细分为三个区域:

新生区(伊甸园区)

养老区

永久区

GC垃圾回收,主要是在伊甸园区和养老区,

假设内存满了,会报OOM,堆内存不够,堆溢出

在jdk8之后,永久存储区改了个名字叫(元空间)

新生区

它是一个类:诞生和成长的地方,甚至死亡;

新生区分为 伊甸园区和幸存者区

伊甸园区;全部的对象都是在伊甸园区里new出来的

幸存区:(0,1)

当伊甸园区满了之后,会触发轻GC,对伊甸园区进行垃圾回收,当某个对象经过GC幸存下来之后,就会进入到幸存者区,依次不断的循环,当幸存0区和1区也满了的时候,在经历过屡次GC之后,活下来的对象,也就是被从新引用的对象就会进入到老年区。而当老年区也满了的时候,就会触发重GC,重CG除了会去清理老年区的,还会伊甸园区和幸存0区1区的全部垃圾全清理掉。而在重GC清除下,活下来的就会进入到养老区。当重GC清理完毕之后,新生区和养老区仍是都满了,这个时候就会报堆溢出的报错。

真理:通过研究,99%对象都是临时对象。

老年区

新生区里面GC不掉的对象就会去到老年区

永久区

这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存。

一个启动类加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM;

jdk1.6以前:永久代,常量池是在方法区之中

jdk1.7:永久代,可是慢慢的退化,去永久代,常量池在堆中

jdk1.8以后:无永久代,常量池在元空间。

逻辑上存在,物理上不存在

堆内存调优

堆内存满了,该如何处理?

public static void main(String[] args) {
    long max = Runtime.getRuntime().maxMemory();
    long total = Runtime.getRuntime().totalMemory();
    System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB");
    System.out.println("total="+max+"字节\t"+(total/(double)1024/1024)+"MB");
}

​ 先尝试把堆内存空间扩大,假如仍是用原来的代码跑,继续包堆溢出的错,咱们就该去考虑考虑本身代码那块有问题,多是有垃圾代码或者是死循环代码,它在不断的占用内存空间

​ 分析内存,看一下那个地方出现了问题(专业工具)

​ 调优代码-Xms1024m -Xmx1024m -XX:+PrintGCDetails

Xms后面填计算机给jvm分配的内存,Xmx后面填Jvm初始的内存值

在一个项目中,忽然出现了OOM故障,那该如何排除研究为何出错~

可以看到代码第几行出错;
Dubug:一行行分析代码!
MAT,Jprofiler做用:

分析Dump内存文件,快速定位内存泄漏;
得到堆中的数据
得到大的对象~
。。。。

GC

jvm在进行垃圾回收时,并非对这三个区域统一回收,大部分时候,回收可是新生代

新生代

幸存区(from,to)

老年区

GC两种类:轻GC(普通的GC),重GC(全局GC)

GC经常使用算法

标记清除法

​ 最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如它的名字同样,算法分为“标记”和“清除”两个阶段:

​ 首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象,之因此说它是最基础的收集算法,是由于后续的收集算法都是基于这种思路并对其缺点进行改进而获得的。

​ 它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。标记-清除算法的执行过程(须要较大内存时却不够了就要回收一次)

复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免过高了一点。

标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操做,效率将会变低。更关键的是,若是不想浪费50%的空间,就须要有额外的空间进行分配担保,以应对被使用的内存中全部对象都100%存活的极端状况,因此在老年代通常不能直接选用这种算法。

分代收集算法(并非一种新的思想,只是将java堆分红新生代和老年代,根据各自特色采用不一样算法)
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并无什么新的思想,只是根据对象的存活周期的不一样将内存划分为几块。通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
新生代–复制算法。老年代–标记-整理算法。

字节码引擎

1.概述
​ Java虚拟机字节码执行引擎是jvm最核心的组成部分之一,全部的 Java 虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果,下面将主要从概念模型的角度来说解虚拟机的方法调用和字节码执行。

2.执行引擎的解释和做用

​ 类加载器将字节码载入内存以后,执行引擎以Java 字节码指令为单元,读取Java字节码。问题是,如今的java字节码机器是读不懂的,所以还必须想办法将字节码转化成平台相关的机器码(也就是系统能识别的0和1)。这个过程能够由解释器来执行,也能够有即时编译器(JIT Compiler)来完成

​ 具体步骤以下图

执行引擎内部包括以下

语法糖

1.概述
​ 语法糖是一种用来方便程序员代码开发的手段,简化程序开发,可是不会提供实质性的功能改造,但能够提升开发效率或者语法的严谨性或者减小编码出错的机会。
​ 总而言之,语法糖能够看做是编译器实现的一种小把戏。

2.泛型和类型擦除

​ 泛型的本质是参数化类型,也就是操做的数据类型自己也是一个参数。这种参数类型能够用在类,接口,方法中,分别叫泛型类,泛型接口,泛型方法。

​ 可是java的泛型是一个语法糖,并不是是真实泛型,只在源码中存在,List和List 在编译以后,就是List 并在相应的地方加上了类型转换代码。这种实现方式叫类型擦除,也叫伪泛型。

可是,擦除法所谓的擦除,仅仅是对方法的code属性中的字节码进行擦除,实际上元数据中仍是保留了泛型信息,这也是咱们能经过反射手段获取参数化类型的根本依据。

泛型:

public class b {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("hello","a");
        System.out.println(map.get("hello"));
    }
}

实际上:

public class b {
    public b(){
        
    }
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("hello","a");
        System.out.println(map.get("hello"));
    }
}

3…自动装箱和遍历循环

自动装箱和遍历循环

public class b {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4);
        for (Integer integer:
             list) {
            System.out.println(integer);
        }
    }
}

实际上

public class b {
    public static void main(String[] args) {
        List<Integer> list =                    Arrays.asList(Integer.valueOf(1),Integer.valueOf(2),Integer.valueOf(3),Integer.valueOf(4));
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = (Integer) iterator.next();
            System.out.println(next);
        }

    }

自动装箱用了Integer.valueOf
for循环用了迭代器

4.条件编译

​ —般状况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,但愿只对其中一部份内容进行编译,此时就须要在程序中加上条件,让编译器只对知足条件的代码进行编译,将不知足条件的代码舍弃,这就是条件编译。

反编译以前

public static void main(String[] args) {
    if(true){
        System.out.println("hello");
    }else{
        System.out.println("beybey");
    }
}

反编译以后

public static void main(String[] args) {
        System.out.println("hello");
}

首先,咱们发现,在反编译后的代码中没有System.out.println(“beybey”);,这其实就是条件编译。

当if(tue)为true的时候,编译器就没有对else的代码进行编译。

因此,Java语法的条件编译,是经过判断条件为常量的if语句实现的。根据if判断条件的真假,编译器直接把分支为false的代码块消除。经过该方式实现的条件编译,必须在方法体内实现,而没法在正整个Java类的结构或者类的属性上进行条件编译。

运行期优化

​ Java 语言的 “编译期” 实际上是一段 “不肯定” 的操做过程,由于它多是指一个前端编译器把 .java 文件转变成 .class 文件的过程;也多是指虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程;还多是指使用静态提早编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器代码的过程

1.解释器与编译器

什么是解释器?

​ 大概意思:

​ 在计算机科学中,解释器是一种计算机程序,它直接执行由编程语言或脚本语言编写的代码,并不会把源代码预编译成机器码。一个解释器,一般会用如下的姿式来执行程序代码:

​ 分析源代码,而且直接执行。
​ 把源代码翻译成相对更加高效率的中间码,而后当即执行它。
​ 执行由解释器内部的编译器预编译后保存的代码
​ 能够把解释器当作一个黑盒子,咱们输入源码,它就会实时返回结果。
​ 不一样类型的解释器,黑盒子里面的构造不同,有些还会集成编译器,缓存编译结果,用来提升执行效率(例如 Chrome V8 也是这么作的)。
​ 解释器一般是工做在「运行时」,而且对于咱们输入的源码,是一行一行的解释而后执行,而后返回结果。

​ 什么是编译器?

​ 源文件通过编译器编译后才可生成二进制文件,编译过程包括预处理、编译、汇编和连接,平常交流中经常使用“编译”称呼此四个过程

2.编译对象与触发条件

"热点代码"分两类,

​ 第一类是被屡次调用的方法-这是由方法调用触发的编译。

​ 第二类是被屡次执行的循环体 – 尽管编译动做是由循环体所触发的,但编译器依然会以整个方法(而不是单独的循环体)做为编译对象。

判断一段代码是否是热点代码,是否是须要触发即时编译,这样的行为称为热点探测(Hot Spot Detection);热点探测断定方式有两种:

​ 第一种是基于采样的热点探测

​ 第二种是基于计数器的热点探测

HotSpot虚拟机中使用的是基于计数器的热点探测方法,所以它为每一个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back EdgeCounter)。肯定虚拟机运行参数的前提下,这两个计数器都有一个肯定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

3.编译优化技术

经典优化技术

语言无关的经典优化技术之一:公共子表达式消除。
语言相关的经典优化技术之一:数组范围检查消除。
最重要的优化技术之一:方法内联。
最前沿的优化技术之一:逃逸分析。
公共子表达式消除

int d= (c * b)*12+a+ (a + b * c)
 
//编译器检测到“c * b”与“b* c”是同样的表达式,并且在计算期间b与c的值是不变的。
        int d=E*12+a+(a+E);

数组边界检查消除

if (foo != null) {
        return foo.value;
        } else {
        throw new NullPointerException();
        }

        # 虚拟机隐式优化;
        try {
        return foo.value;
        } catch (Segment_Fault e) {
        uncommon_trap(e);

4 Java与C/C++的编译器对比

第一,由于即时编译器运行占用的是用户程序的运行时间,具备很大的时间压力,它能提供的优化手段也严重受制于编译成本

第二,Java语言是动态的类型安全语言,这就意味着须要由虚拟机来确保程序不会违反语言语义或访问非结构化内存

第三,Java语言中虽然没有virtual关键字,可是使用虚方法的频率却远远大于C/C++语言

Java内存模型与线程

1. 硬件效率与一致性

除了增长高速缓存以外,为了使得处理器内部的运算单元尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算以后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的前后顺序与代码中的顺序一致。

2. Java内存模型

​ 主内存与工做内存

​ Java内存模型的主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

​ 为了获取较好的执行效能,Java内存模型并无限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化操做。

​ 内存间的交互操做

​ lock(锁定):做用于主内存的变量,它把一个变量标志为一条线程独占的状态。
​ unlock(解锁):做用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁 定。
​ read(读取):做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用。
​ load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
​ use(使用):做用于工做内存的变量,它把工做内存中一个变量的值传递给执行引擎。
​ assign(赋值):做用于工做内存的变量,它把一个从执行引擎接受到的值赋给工做内存的变量。
​ store(存储):做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存中,以便随后的write操做使用。
​ write(写入):做用于主内存中的变量,它把store操做从主内存中获得的变量值放入主内存的变量中。

​ 原子性、可见性与有序性

​ 原子性(Atomicity):由Java内存模型来直接保证的原子性变量操做包括read、load、assign、use、和write。

​ 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其它线程可以当即得知这个修改。J
​ 有序性(Ordering):若是在本线程内观察,全部的操做都是有序的;

3.Java与线程

​ 线程的实现

​ 使用内核线程实现

​ 内核线程(Kernel-Level Thread,KLT)就是直接由操做系统内核支持的线程,这种线程由内核来完成线程切换,内核经过操做系统调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每一个内核线程能够视为内核的一个分身,这样操做系统就有能力同时处理多件事情,支持多线程的内核就叫多线程内核。

使用用户线程实现

​ 从广义上来说,一个线程只要不是内核线程,就能够认为是用户线程,所以,从这个定义上来说,轻量级进程也属于用户线程,但轻量级进程的实现始终是创建在内核之上的,许多操做都进行系统调用,效率会受到限制。
而狭义上的用户线程指的是彻底创建在用户空间的线程库上,系统内核不能感知线程存在的实现。
​ 使用用户线程的优点在于不须要系统内核的支援。劣势也在于没有系统内核的支援,全部的线程操做都须要用户程序本身处理。

使用用户线程加轻量级进程混合实现

​ 在这种混合模式下,即存在用户线程,也存在轻量级进程。用户线程仍是彻底创建在用户空间中,所以用户线程的建立、切换、析构等操做依然廉价,而且能够支持大规模的用户线程并发

4.Java线程的状态转化

新建(New):建立后还没有启动的线程处于这种状态。
运行(Runable):Runable包括了操做系统线程状态中的Running和Ready,也就是说处于此种状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。
无限期等待(Waiting):处于这种状态下的线程不会被分配CPU执行时间,他们要等待被其余线程显示唤醒。
限期等待(Timed Waiting):处于这种状态下的线程也不会被分配CPU执行时间,不过无须等待被其余线程显示唤醒,在必定时间以后它们由系统自动唤醒。
阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生。
结束(Terminate):已经终止的线程的线程状态,线程已经结束执行。

线程安全与锁优化

1.线程安全

​ 当多个线程访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,或者在调用方进行任何其余的协调操做,调用这个对象的行为均可以得到正确的结果,那这个对象是线程安全的

2.Java 语言中的线程安全

​ 2.1不可变

​ 在 Java 语言中,不可变线程必定是安全的,不管是对象的方法实现仍是方法的调用者,都不须要采起任何的线程安全保障措施

​ 其中最简单的就是把对象中带有状态的变量都声明为 final,这样在构造函数结束以后,它就是不可变的

​ 2.2绝对线程安全

private static Vector<Integer> vector = new Vector<Integer>();  
1
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}

    Thread removeThread = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (int i = 0; i < vector.size(); i++) {  
                vector.remove(i);  
            }  
        }  
    });  
      
    Thread printThread = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            for (int i = 0; i < vector.size(); i++) {  
                System.out.println(vector.get(i));  
            }  
        }  
    });  
      
    removeThread.start();  
    printThread.start();  
      
    // 不要同时产生过多的线程,不然会致使操做系统假死  
    while (Thread.activeCount() > 20);  
}  
}

3.相对线程安全

相对的线程安全就是咱们一般意义上所讲的线程安全,它须要保证对这个对象单独的操做是线程安全,咱们在调用的时候不须要作额外的保障措施,可是对于一些特定顺序的连续调用,就可能须要在调用端使用额外的同步手段来保证调用的正确性。

4.线程兼容

线程兼容是指对象自己并非线程安全的,可是能够经过在调用端正确地使用同步手段来保证对象在并发环境中能够安全地使用,咱们日常说一个类不是线程安全的,绝大多数时候指的是这一种状况。

5.线程对立

线程对立是指不管调用端是否采起了同步措施,都没法在多线程环境中并发使用的代码。因为 Java 语言天生就具有多线程特性,线程对立这种排斥多线程的代码是不多出现的,并且一般都是有害的,应当尽可能避免。

线程安全的实现方法

1.互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用,而互斥是实现同步的一种手段,互斥是方法,同步是目的

2.非阻塞同步

测试并设置,获取并增长,交换,比较并交换,加载链接 / 条件存储

3.无同步方案

​ 要保证线程安全,不必定非要保证线程同步,还能够有其余的方案

​ 1.可重入代码
​ 2.线程本地存储

锁优化

自旋锁和自适应自旋锁

若是锁在很短的时间内释放了,那么自旋的效果就很好

偏向锁

​ 偏向锁的意思是这个锁会偏向第一个获取到他的锁,若是在接下来执行的过程当中,该锁一直没有被其余的锁获取的话,则持有偏向锁的线程永远不须要再进行同步.一旦有新的线程试图获取该锁,偏向模式会被撤销.撤销后进入无锁状态.这里会改变对象头的关于偏性模式的标志位和关于锁的标志位

轻量级锁
当使用轻量级锁(锁标识位为00)时,线程在执行同步块以前,JVM会先在当前线程的栈桢中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中

锁粗化

​ 这个原则大部分时间是对的可是若是一个系列的连续操做都是对同一个对象反复的加锁和解锁,甚至加锁操做出如今循环体之中,即便没有线程竞争,频繁的进行互斥同步的操做也会致使没必要要的性能损耗.

锁消除

public String concatString(String s1, String s2){
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

​ 咱们发现sb的引用被限制在了concatStirng方法里面他永远不可能被其余线程访问到,所以虽然这里有锁可是能够被安全的消除掉.在解释执行时这里仍然会枷锁,可是通过服务端编译器即时编译后,这段代码会自动忽略全部的同步措施直接执行.

最后

你们看完有什么不懂的能够在下方留言讨论,也能够关注我私信问我,我看到后都会回答的。也欢迎你们关注个人公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,以为文章对你有帮助的话记得关注我点个赞支持一下!

相关文章
相关标签/搜索