转载

浅谈JVM虚拟机

本人是一名前不久被秋招打得体无完肤的Java小白,在经历过秋招以后,认识到JVM虚拟机的重要性,虽然之前了解过虚拟机的一些基本知识和概念,但是对于怀揣这进入大厂的心的我是远远不够的,于是想更深一步的了解并学习JVM,《深入理解JAVA虚拟机》这本书对JVM剖析的特别清楚,最近也在一直看,总结了一些个人认为非常不错的东西,记录分享给大家,算是对知识的一个掌握,希望对大家有所帮。

JVM内存结构以及内存溢出的情况

Java运行时数据区

浅谈JVM虚拟机
程序计数器、虚拟机栈、本地方法栈是线程私有内存
方法区和堆是线程共享数据区
复制代码

1)程序计数器

程序技术器是一个较小的内存空间,可以看作当前线程所执行字节码的行号指示器。

通过改变字节码解释器的值确定下一跳需要执行的字节码指令。

JVM多线程通过轮流切换并分配处理器执行时间实现。

一个处理器(多核处理器的一个内核)只会执行一条线程中的指令。

每条线程都有一个独立的程序计数器。

执行Java方法时,记录正在执行的JVM字节码指定地址。

执行native方法,计数器为空。

JVM在此区域没有规定任何OutOfMemortError。  
复制代码

2)虚拟机栈

生命周期与线程相同,描述Java方法执行的内存模型。

方法执行时,创建栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。

方法从开始调用至调用完成,对应着栈帧的入栈到出栈。

局部变量表存放编译时期可知的基本数据类型、对象的引用、returnAddress(指向一条字节码指令的地址)。

64位的long和double站两个局部变量空间,其余占一个。

局部变量表的内存空间在编译器确定,运行期不会改变。

线程请求栈深度大于JVM规定时,报StackOVerFlowError  
若栈动态扩展时无法申请到足够的内存空间,报OutOfMemoryError
复制代码

3)本地方法栈执行Native方法

Sun HotSpot将虚拟机栈和本地方法栈合二为一

异常情况和虚拟机栈相同
复制代码

4)堆(Heap) 堆是JVM中占用内存最大的区域,它不需要连续的内存空间,在JVM启动时创建。

堆用来存放对象实例。

垃圾回收器的主要管理区域,也被称为GC堆。

由于目前主流虚拟机使用分代收集机制,所以堆还可以细分为新生代、老年代。

线程共享的堆还可以分为多个线程私有分配缓冲期(Thread local Allocation Buffer简称TLAB)。

若堆中没有内存完成实例分配,且无法再进行扩展,报OutOfMemoryError
复制代码

5)方法区

用于存储已被JVM加载的类信息、常量、静态变量、即使编译的代码。

方法区一开始由永久代来实现

使用永久代来实现方法区更容易早层内存溢出。

在jdk1.7之后,将字符串常量池从永久代中移除。

jdk1.8之后,移除永久代,引入metaSpace。

方法区不需要连续的内存空间,可扩展,可以不实现垃圾回收。

当方法区无法满足内存分配需求时,报OutOfMemoryError
复制代码

6)运行时常量池存放编译时期产生的各种字面量和符号引用,、翻译出来的直接引用。

在类加载后进入方法区的运行时常量池存放。

在运行期可以将新的常量放入池中,例如String的intern方法。( 这里可以参考我的另外一篇关于String的博客

常量池无法再申请到内存时,报OutOfMemoryError
复制代码

对象的创建过程

1)JVM收到一条new指令时,会检查这个指令携带的参数在常量池中能否被找到对应的符号引用,再检查该符号引用对应的类是否已被加载过,若没有,先进性类加载。

2)类加载完成后,为对象分配内存,对象所需内存在类加载后完全确定。

  • 分配内存有两种方式:
  • 指针碰撞
    当heap内存分配十分规整时(即空闲内存和已被使用内存由一个指针完全分开)时,分配内存时只是把指针向空闲的一侧移动与对象大小相等的距离。
  • 空闲列表
    若内存分配不规整,JVM会维护可用内存列表,分配时找一块足够大的内存划分给对象。
  • heap内存是否规整取决于垃圾回收器是否带有压缩整理功能
  • 分配内存时线程安全问题解决方案
    对分配内存空间动作进行同步处理:采用CAS+失败重试的方法保证更新操作的原子性。
    把内存分配的动作按线程划分在不同的空间中:为每个线程分配本地线程分配缓冲区(TLAB)。

3)分配完内存,将分配到的内存空间初始化为零值(不包括对象头),保证对象实例不赋初值便可直接使用。

4)设置对象头。

5)new指令后,执行init方法。

对象的内存布局

对象头布局:对象头、实例数据、对其填充。

  • 1):对象头
    对象自身的运行时数据,包括哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有锁、偏向进程ID、偏向时间戳等。
    类型指针:JVM通过指针确定该对象是哪个类的实例。
    若对象是数组,对象头记录数组大小,因为数组的大小无法从对象元数据中获得。
  • 实例数据
    代码中定义的各种类型的字段内容,包括从父类继承的。
  • 对其填充
    没有特别含义,占位符作用,因为JVM规定对象起始地址必须是8字节的整数倍,而对象头部分大小正好是8字节整数倍。

对象的访问定位

Java程序要通过栈上的reference数据操作堆中的具体对象

对象的访问方式有两种:使用具柄、直接指针

**句柄:**heap划分出一块内存作为句柄池,reference存储的是对象的句柄地址,句柄中包含对象实例数据和对象类型数据的地址信息。

浅谈JVM虚拟机

直接指针:reference存储的是对象地址

浅谈JVM虚拟机

使用句柄的好处是当对象需要移动时,reference无需改动,只改变句柄中的实例数据指针

直接指针速度快,节省了一次指针定位的时间开销,Sun HotSpot使用直接指针

垃圾回收与内存分配策略

GC是什么?为什么要有GC?

GC是垃圾回收,内存是编程人员更容易出现问题的地方,忘记或错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动检测对象是否超过了作用域而达到自动回收的目的。

程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭,这三个区域的内存分配和回收具有可确定性,当方法结束或者线程结束,内存自然跟着回收。

而heap和方法区的内存分配是动态的,因此需要垃圾回收机制。

判断对象是否死去:不可再被任何途径使用的对象被判定为死去。

简述JAVA的GC机制

在Java中,程序员不需要手动的去释放一个对象的内存,而是由JVM自行执行。

在JVM中,有一个低优先级的垃圾回收线程,正常情况下不会执行,只有在JVM空闲或者内存不足时,才会触发执行,扫描那些没有任何引用的对象,并把他们添加到要回收的集合中进行回收。

判断对象是否死去的方法有引用计数算法和可达性分析算法

1):引用计数算法

给对象添加一个引用计数器,每当有一个地方引用该对象,计数器+1,引用失效,计数器-1

任何时刻计数器为0的对象就是不可能再被使用的对象,认为该对象死亡。

但是引用计数器很难解决对象之间相互循环引用的问题

2):可达性分析算法

主流语言使用可达性分析算法实,判断对象是否存活。

通过GC Roots对象作为起始点,从这些节点向下搜索,搜索所走过的路径成为引用链

当一个对象到GC Roots没有任何引用链时(即GC Roots到该对象不可达),证明该对象不可用,将被判定为可回收对象。

可作为GC Roots的对象

虚拟机栈中(栈帧的局部变量表)引用的对象  
方法区中静态属性引用的对象  
方法区中常量引用的对象  
本地方法栈中JNI(一般指Native)引用的对象  
复制代码

引用

jdk1.2之前,若reference类型的数据存储的数值代表的是另外一块内存的起始地址,称这块内存代表一个引用。

jdk1.2之后引用分为 强引用、软引用、弱引用、虚引用

  • 强引用:指在代码中普遍存在的例如"Object obj = new Object()",只要强强引用存在,该对象永远不会被回收。
  • 软引用:用来描述有用但是非必须的对象,JVM会在内存溢出之前把这些对象放列进待回收范围并进行二次回收,若回收之后还是没有足够的内存,抛出内存溢出异常,jdk1.2之后,使用softReference类实现软引用。
  • 弱引用:描述非必需对象,强度比软引用更弱,被软引用关联的对象只能生存到下一次垃圾回收器执行之前,无论内存是否足够,都会被回收,jdk1.2之后使用weakReference实现弱引用。
  • 虚引用:最弱的引用关系,不回对对象的生存时间构成影响,也无法通过虚引用取得对象实例,唯一的作用是在对象被回收时收到系统的通知,jdk1.2之,使用PhantomReference实现虚引用。

宣告一个对象死亡的两次标记

1)对象在可达性分析后发现没有与GC Roots相连接,进行第一次标记并进行第一次筛选

筛选条件是判断该对象有没有必要执行finalize方法

若该对象没有重写finalize方法或者JVM已经调用过该方法,则认为没有必要执行

若判定为有必要执行,对象会被放入F-Queue队列,JVM自动建立一个低优先级的finalizer线程执行

执行是指JVM只能保证该线程被启动,但是不能保证线程执行结束或成功

原因是如果某个对象的finalize方法执行非常缓慢或者发生死循环,会导致其他对象一直等待,甚至导致整个内存崩溃。

2)稍后GC对F-Queue进行二次标记 若对象此时与GC Roots相连接,会被移除待回收对象集合 ,否则被回收。

任何对象的finalize方法只会被调用一次,而且finalize方法并不是C或者C++的析构函数,运行代价很高,不确定因素大,无法保证对象的调用顺序,使用try-finally可以做的更好!

回收方法区

JVM可以不要求在方法区中进行垃圾回收,在方法区进行垃圾回收"性价比很低"。

永久代的垃圾回收主要包括: 废弃常量,无用类

  • 废弃常量
    与回收heap中的对象类似
  • 回收类 :判断是否为"无用类"
    该类所有实例被回收,heap中没有该类的任何实例
    用于加载该类的classLoader被回收
    该类对象的Java.lang.class对象没有在任何地方被引用
    无法在任何地方通过反射来加载该类的方法

垃圾回收算法

最基础的: 标记-清除算法

分为标记和清除两个阶段

不足

  • 效率低
  • 空间(标记清除后产生大量的不连续内存碎片)

复制算法

将内存分为相等的两块区域,每次只使用其中的一块,当一块内存使用完,将其中存活的对象复制到另一块,然后将已经使用的内存空间一次清理掉。

代价是将内存缩小了原来的一半。

HotSpot并没有按照1:1的比例划分空间

HotSpot将内存划分为一块较大的Edeh空间和凉快较小的Servivor空间,Eden:Servivor=8:1

回收时,将Eden和Servivor中存活的对象一次性复制到另一块Servivor中,最后清理掉用过的Eden和Servivor

但是无法保证每次只有不超过百分之十的对象存活,当Servivor不够用时,依赖其他内存空间(老年代),这是需要进行分配担保。

标记-整理算法

复制算法在对象存活率较高时要进行较多的复制操作,效率低,在老年代中使用标记-整理算法

标记过程与标记-清除算法一致

后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界之外的内存。

分代收集算法

根据对象存过周期的不同将heap内存划分为新生代和老年代

  • 新生代中每次都有大量的内存死亡,只有少数内存可以存活,选择复制算法
  • 老年代中对象存活率较高,使用标记-清除算法或者标记-整理算法

HotSpot的算法实现

枚举跟节点

可达性分析从GC Roots找引用链会消耗很多时间

可达性分析对执行时间的敏感度还体现在GC停顿上,在整个分析过程中,不可以出现分析过程中对象引用关系发生变化的情况,导致GC进行时必需停掉所有的Java进程(Stop the World 简称STW)

目前使用的JVM都是准确式GC,在执行系统停顿下来后,不需要检查完所有的执行上下文和全局引用位置,在HotSpot中使用了一组OopMap的数据结构,在类加载完成时,HotSpot把对象内什么偏移量对应什么类型记录下来,在JIT编译过程中,会在特定的位置记录下栈和寄存器中那些位置上是引用。

安全点(SofePoint)

OopMap使得HotSpot可以快速准确的完成GC Roots枚举

但如果每一条指令都声称OopMap,需要大量的额外空间,GC空间成本提高,所以HotSpot只在安全点上生成OopMap

安全点的选定基础:**"是否有让程序长时间执行"**为标准,"长时间执行"最明显的特征就是指令序列的复用(例如方法调用、循环跳转、异常跳转等)

如何在GC时让线程在安全点停顿

  • 抢险式中断:GC发生时,所有线程中断,若发现有线程不再安全点,恢复线程,直到线程到达安全点。
  • 主动式中断:GC在检查点和创建对象需要分配内存的地方设置轮循标志,线程执行时主动轮循该标记,若发现中断请求为true,则把自己中断挂起。

安全区域

softPoint保证了程序执行时,在不太长的时间里会遇到可进入GC的softPoint,但当线程没有分配到CPU资源时,例如线程处于sleep状态,无法相应JVM的中断请求,这是需要safe Region(安全区域)

safe region指在一段代码中,引用关系不会发生变化

在线程执行到safe region中,首先标识已进入safe region

在jvm发起GC请求时,不用管在safe region中的代码

线程离开safe region时,检查跟节点枚举是否完成或者GC是否完成,若完成,线程继续执行,否则一直等待收到可以离开safe region的信号后继续执行。

垃圾收集器的基本原理?可以立即回收吗?可以手动通知JVM进行gc吗?

对于GC,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况,通常GC采用有向图的方式记录和管理heap中的所有对象。

通过这种方式确定对象是否"可达",若GC确定不可达,GC就回收这些内存空间

可以手动执行System.gc,通知GC运行,但是Java语言不保证gc一定会执行。

若设置对象的引用为null,gc会立即执行吗?

不会,在下一个gc周期中,这个对象可被回收

什么是分布式垃圾回收(DGC),如何工作?

DGC称为分布式垃圾回收,RMI使用DGC来做自动垃圾回收,因为RMI包含了跨JVM的远程对象的引用,垃圾回收很困难

DGC使用引用技术算法给远程对象提供自动内存管理。

垃圾收集器

并行和并发

并行:多条垃圾回收线程并行工作,但是用户还是处于等待状态  
并发:指用户与垃圾回收线程同时执行,不一定并行,可能交替执行
复制代码

1)serial收集器

  • 单线程收集器,只会使用一个cpu或者一条手收集线程,且在垃圾回收时,必需暂停所有工作线程,直到收集结束
  • 简单高效(与其他收集器的单线程比)
  • serial对于运行在client模式下的JVM是一个很好的选择

2)ParNew收集器

  • ParNew是serial的多线程版本
  • 是许多运行在server模式下的JVM首选的新生代收集器
  • 只有serial和parNew可以配合CMS
  • 默认开启的收集线程数与CPU的数量相同

3)Parrller Scavenge收集器

  • 并行,新生代收集器,使用复制算法
  • 目的是达到一个可控的吞吐量(CPU运行用用户代码时间/CPU总消耗时间)
  • 适合在后台计算而不需要太多交互的任务

4)Serial Old收集器

  • serial的老年代版本,单线程,使用标记-整理算法
  • 给client模式下的JVM使用

5)Paraller Old收集器

  • 多线程
  • 使用标记-整理算法
  • 老年代中使用
  • jdk1.6之后提供
  • 注重吞吐量以及CPU资源敏感的场合,优先考虑 Paraller Scavenge+Paraller Old

6)CMS收集器

-以获取最短回收时间为目标的收集器

适合重视服务的相应时间、系统停顿最短的应用

使用标记-清除算法

CMS的四个阶段

  • 初始标记:仅仅标记GC Roots能直接联系到的对象,速度很快
  • 并发标记进行 GC Roots Tracing
  • 重新标记是为了修正并发标记时因用户几乎工作而导致的标记产生变动的那一部分对象的标记记录
  • 并发清除
  • 并发标记和并发清除可以与用户线程一起工作

CMS的缺点

  • CMS对cpu资源非常敏感,虽然不会大导致用户线程停顿,但是会因占用了一部分线程资源导致用户进程缓慢,吞吐量下降
  • CMS无法处理浮动垃圾
  • 由于CMS使用“标记-清除”算法,收集结束后会有大量的空间碎片产生

7)G1收集器

面向服务端的垃圾收集器,应用在多核处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集器的暂停时间请求

向CMS一样可以与应用并发执行

G1同CMS相比的优点

  • G1垃圾收集器是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
  • G1的Stop the World更可控,G1在停顿上添加了预测机制,用户可指定期望的停顿时间
  • G1将整个heap划分为多个大小相等的独立区域(region),虽然保留新生代和老年代概念,但不再是物理隔层了
  • G1可以充分利用CPU、多核环境硬件优势,尽量缩短STW
  • G1整体使用标记-整理算法,局部使用复制算法,不会产生内存碎片
  • G1不存在物理层面上的新生代和老年代

G1回收步骤

  • 初始标记:标记一下GC Roots能直接关联的对象,需停顿线程,但耗时很长
  • 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出来存活的对象,耗时较长,但是可与程序并发执行
  • 最终标记:修改在并发标记时因用户程序继续运行而导致标记产生变动的那一些因素
  • 筛选回收:对各个region的回收价值进行排序,根据用户所期望的GC停顿时间指定回收计划

对象的内存分配

对象主要分配在新生代的Eden区上

若启动了本地线程分配缓冲区,按线程优先级分配在TLAB上

TLAB(Thread Local Allocation Buffer):线程本地分配缓冲区

少数情况下直接分配在老年代

  • 对象优先在Eden上分配,大多数情况下对象在新生代的Eden上分配,当Eden没有足够的空间进行分配时,JVM发起Minor GC(新生代GC)
  • 大对象直接进入老年代(参数:-XX:PretenursSizeThreshold)
    大对象指需要大量连续内存空间的Java对象(例如数组、长字符串)
    经常出现大对象容易导致内存还有不少空间时就提前触发GC获取足够的连续空间
    大对象放入老年代是防止在Eden以及两个Servivor区之间发生大量的内存复制
  • 长期存活的对象进入老年代
    JVM给每个对象定义了对象年龄计数器
    对象在Eden出生并经过第一次minor GC后仍存活,并可以被Survivor容纳,会被移入Servivor中,且对象年龄为1
    对象每熬过一次minor GC,年龄+1,增加到晋升老年代阈值(默认为15),会被晋升到老年代中,可以通过(-XX:MaxTenuringThreshold调整)
  • 若在Servivor中相同年龄的对象总和大于servivor的一半,年龄大于或等于该年龄的对象可进入老年代

空间分配担保

在发生minor GC之前,JVM检查老年代中最大的连续空间是否大于新生代所有对象总空间

若大于,minor GC确保安全

若不成立,JVM查看HandlePromotionFailure设置是否允许担保失败

若允许,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

若大于,尝试进行Minor GC,但是会有风险

若不大于或HandlePromotionFailure不允许担保失败,这是改为进行一次Full GC(老年代GC)

风险是指HandlePromotionFailure担保失败,失败后进行Full GC

jdk1.6之后,HandlePromotion不再使用,默认允许担保失败

JAVA存在内存泄漏吗

内存泄漏是指一个再被使用的对象或变量一直被占据在内存中

内存泄漏情况

  • 长生命周期对象持有短生命周期对象的引用可能导致内存泄漏,例如缓存系统
  • 缓存系统
    加载一个对象到缓存,然后一直不实用,这个对象一直被缓存,但是不再被使用
  • 当一个对象被放入HashSet,若修改了这个对象中用于计算Hash的参数,将会导致无法从HashSet中单独删除当前对象,造成内存泄漏。

System.gc和Runtime.gc有什么用

提示JVM要进行垃圾回收,但立刻开始还是延迟还取决于JVM

Java类加载机制

Java把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型

Java语言中,类加载是动态的,并不会一次性将所有的类全部加载后在运行,而是保证程序运行的基础类完全加载到JVM中,其他类在需要时加载,为Java应用程序提供高度灵活性。

类从被加载到JVM内存中开始,到卸载出内存为止
生命周期包括:加载、验证、准备、解析、初始化、使用、卸载
验证、准备、解析三部分成为连接
加载、准备、解析三部分成为连接
加载、验证、准备、初始化、卸载顺序确定,而在某些情况下,解析可能在初始化之后进行
这些阶段通常是互相交叉的混合进行,通常一个阶段执行过程中会激活另外一个阶段

执行初始化的五种情况

  • 遇到new、getstatic、putstatic、invokestatic时,若类没有初始化,需要先触发初始化
    使用new实例类对象时
    读取或设置类的静态字段(被final修饰,已在编译器就把结果放入常量池的static字段除外)
    调用静态方法时
  • 使用Java.lang.reflect包的方法对类进行反射调用时,若类没有初始化,需要先初始化
  • 初始化类时,若父类没初始化,先初始化父类
  • JVM启动时,用户需指定一个执行主类(包含main方法的类),Java先初始化这个类
  • 使用jdk1.7的动态语句支持时,若一个java.lang.invoke.MethodHandle实例最后的解释结果为REF_getstatic、 REF_putstatic、REF_invokestatic的方法句柄,且这个句柄对应的类没进行初始化,则先触发初始化
  • 这5种成为主动引用,除此之外,所有引用类的方式都不会触发初始化,成为被动引用
  • 接口的加载过程与类加载有所不同
  • 接口不能使用static代码块
  • 在接口的初始化中,不要求父接口全部都完成序列化,只有在真正使用到父类的时候(例如引用接口中定义的常量)才会被初始化

类加载的过程

1)加载

  • 把Java字节码加载成一段二进制流,读取到内存,放在运行时数据的方法区内,创建一个Java.lang.Class对象描述该类的数据结构
  • 可以从磁盘jar、war、网络、自己编写的class文件中加载class文件

2)验证

  • 确保类加载的正确性,保证class文件的字节流不会影响JVM的安全验证
  • 文件格式的验证:例如验证文件开头魔数代表的jdk版本,常量池中是否有不支持的常量等
  • 元数据验证:验证类是否有父类,是否继承了不允许继承的类,是否实现了父类或接口中要求实现的方法等..
  • 字节码验证:对类的方法体验证,保证类型转换正确
  • 符号引用验证:发生在符号引用转换为直接引用时,确保该符号引用可以找到对应主类
  • 若验证失败,抛出VerifyError,验证通过就把内存中的二进制流存放到JVM的运行数据区的方法体中

3)准备

  • 为类的静态变量分配内存,并将其初始化为默认值

4)解析

  • 将虚拟机常量池中的符号引用转换为直接引用

5)初始化

  • 被动使用不会导致类的初始化
  • 为静态变量赋初始值,执行static块
  • 类的初始化方法,JVM第一次加载class文件调用
  • 实例初始化方法,实例创建出来的使用调用

类加载器

类加载器用于实现类的加载动作

任意一个类,都需要通过加载它的类加载器和这个类本身一同确立在Java虚拟机中的唯一性

每一个加载器都有一个独立的类名称空间

类加载器责任范围

  • BOotStrap classloader 不是Java类,有C++实现,负责在Java启动时加载jdk核心类库,完全有jvm控制
  • Extension classloader 普通Java类,继承自classloader类,负责加载jre/lib/ext下的所有jar包
  • App classloader Extension classloader的子对象,负责加载应用程序classpath下的所有jar和class文件

两种类加载方式

  • classForName:保证一个Java类有效的加载到内存,类默认初始化即执行内部的static代码块保证静态属性初始化,默认使用当前类加载器加载对应的类
  • CLassLoader.loadClass:由于双亲委派的存在,最终把任务交给BootStrap ClassLoader进行加载,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

双亲委派模型

浅谈JVM虚拟机

双亲委派是指每次收到类加载请求,先将请求委派给父类加载器完成(所有加载器最终会委派给顶层的BootStrap ClassLoader加载器中),如果父类加载器无法完成这个加载(该加载器搜索范围中没有找到相应类),子类尝试自己加载,若都没有找到,报ClassNotFoundException异常。

为什么使用双亲委派

  • 避免重复加载,Java类随着它的类加载器(也可以说是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。

双亲委派模型的破坏

jdk1.4之后,mysql注册Driver的过程

获取com.mySql.jdbc.Driver全类名  
Class.forName加载
复制代码

分析

CLassFormat加载使用的调用者的CLassLoader,而调用者DriverManager是在re.jar包总的加载器,CLassLoader是类加载器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

那么必需在启动类加载器中获取应用程序加载去就可以去加载,这就是线程上下文加载器。

线程上下文加载器

线程上下文类加载器可以通过 Thread.setContextClassLoaser() 方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器

很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则

JAVA热部署实现 HotSwap

热部署是在不重启JVM的前提下,可以自动侦测到Class的变化,更新运行时的class行为

实现热部署的两种方式

  • 修改JVM源码,改变classloader加载行为,使JVM可以监听到class文件的更新,重新加载class
  • 创建自定义的classloader,加载需要监听的class

步骤

  • 销毁自定义的classloader(被该加载器加载的class也会自动卸载)
  • 更新class
  • 使用新的classloader加载class

自定义加载器

  • 继承Java.lang/CLassLoader 重写findclass方法
  • 若想遵守双亲委派,重写classfind方法
  • 若想破坏双亲委派,重写loadclass方法

Java内存模型与线程

Java内存模型(JMM)

  • Java内存模型的目标是定义程序中各个变量的访问规则,变量不包括局部变量和方法参数,因为这些是线程私有的。
  • Java内存模型规定所有变量都存在于主线程,每条线程有自己的工作内存,工作内存保存了被该线程使用到的变量祝内存副本拷贝,线程对变量的所有操作必需在工作内存中进行,不能直接读写祝内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。
浅谈JVM虚拟机

内存间交换操作

把一个变量从主内存拷贝到工作内存或把工作内存中的变量同步到主内存,要保证操作的原子性

  • lock:作用域主内存变量,把一个变量表示为一条线程独占的状态
  • unlock:作用域主内存变量,把一个处于锁状态的变量释放出来,释放后才可以被其他线程锁定
  • read:作用于主内存中的变量,把一个变量的值从主内存中传输到线程的工作内存,以便随后的load操作
  • load:作用于工作内存中的变量,把read操作从主内存得到的变量放入工作内存的变量副本中
  • use:作用于工作内存:把工作内存的一个变量的值传递给执行引擎,每当虚拟机遇到一个使用到变量的值的字节码指令时执行该操作
  • assign:作用于工作内存:把一个从执行引擎接收到的值赋给工作内存的变量,每当JVM遇到一个给变量赋值的字节码指令时,执行该操作
  • store:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便后续的write操作
  • write作用于主内存中的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

执行上述操作的规则

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

Volatile

被volatile修饰的共享变量:
保证了不同线程对该变量操作的可见性
禁止指令重拍序
复制代码

JMM

JVM定义了一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java可以在各种平台上达到一致的内存访问效果

JMM主要就是围绕如何在并发过程中处理原子性、可见性和有序性而建立的,通过解决这三分问题,可以解决缓存不一致的问题

原子性

让一个操作不可中断,要么全部执行,要么全部执行失败,即使在多线程环境下,一个操作一旦开始,不会被其他线程干扰

让volatile满足原子性的条件:

  • 变量的改变不依赖于变量本身
  • 变量不需要与其他的状态变量共同参与不变约束

有序性

  • synchronized:语义是表示在同一时刻,只能有一个线程获取,当前锁被占用,其他线程只能等待,因此synchronized语义就是线程在访问读写共享变量时只能“串行”执行,所以synchronized具有有序性
  • volatile:JMM为了性能优化,编译器和处理器会进行指令重排序,Java天生的有序可总结为:
    如果在本线程内观察,所有操作是有序的
    如果一个线程观察另外一个线程,所有操作都是无须的
  • 而volatile可以禁止指令重排序,所以具有有序性

可见性

指一个线程修改了共享变量,其他线程可以立即得到这个指

JMM是通过在变量修改后将新值同步回主内存,在变量读取前从主内存中刷新变量值这种方式实现

而volatile保证了新值可以立即同步到主内存,以及每次使用前可以立刻从主存中刷新,则volatile保证了可见行

synchronized和final也具有可见行

  • synchronized对一个变量执行unlock前,必须先把此变量同步回主存中
  • final:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那么其他线程可以看到final字段的值

先行发生原则 happens-before

判断数据是否存在竞争,线程是否安全的主要依据

先行发生原则指:若操作A先行与线程B,操作A产生的影响能被操作B"观察到"

影响指包括修改了内存中共享变量的值、发送了消息、调用方法等

原文  https://juejin.im/post/5dec8a536fb9a01638079b5d
正文到此结束
Loading...