JVM
1. JVM(Java Virtual Machine)Java虚拟机
常见虚拟机:JVM、VMwave、Virtual Box等,通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机体系。JVM是通过软件模拟Java字节码的指令集,另外两个是通过软件模拟物理CPU的指令集。
2. 内存区域划分
JVM会在执行Java程序的过程中把它管理的内存区域划分为若干个不同的数据区域,其中线程私有区域:程序计数器、Java虚拟机栈、本地方法栈;线程共享区域:Java堆、方法区、运行时常量池。
2.1 程序计数器
程序计数器是一块比较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,即当前线程跑到哪一行代码。如果当前线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果当前线程正在执行的是一个本地Native方法,则这个计数器的值为空。由于多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令,因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各个线程之间计数器互不影响。
注意:程序计数器内存区域是唯一一个在JVM规范中没有规定任何OOM(Out Of Memory)
情况的区域。
2.2 Java虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧用来存储局部变量表、操作数栈、动态链接等信息。每个方法从调用到执行完成的过程就对应栈帧的入栈出栈,生命周期随线程。其中参数:Xss
设置栈深度。
局部变量表:存放编译器可知的八大基本数据类型和对象引用。所需空间大小在编译期间分配,执行期间大小不会改变。
异常:(1)当线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverFlowError
异常;(2)虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemory)
异常。
2.3 本地方法栈
类似于Java虚拟机栈,不同之处在于本地方法栈服务于虚拟机使用的本地方法,Java虚拟机栈服务于JVM执行的Java方法。
2.4 堆
堆是JVM管理的最大的内存区域,在JVM启动时创建,存放的都是对象实例,即所有的对象实例以及数组都要在堆上分配。同时堆也是垃圾回收的主要区域,也称为GC堆,可以处于物理上不连续的内存空间中。其中几个参数:Xms
:设置堆的最小值,Xmx
:设置堆的最大值,Xmn
:设置新生代内存大小。
异常: 当堆中没有足够的内存完成实例分配并且堆也无法扩展时,会抛出OOM
异常。
2.5 方法区
用于存储已被虚拟机加载的类信息(即每个类的权限、属性、方法、类型)、常量、静态变量等数据。此区域的垃圾回收主要针对常量池的回收以及对类型的卸载。
异常: 当方法区无法满足内存分配需求时,会抛出OOM
异常。
2.6 运行时常量池
运行时常量池是方法区的一部分,存放字面值(字符串、final常量、基本数据类型的值)和符号引用(类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符,即包名.类名就是符号引用)。
3. 内存泄漏与内存溢出
(1)内存泄漏:泄漏对象无法被GC;
(2)内存溢出:内存对应确实还应该存活,此时要根据JVM堆参数与物理内存比较检查是否要对堆内存做调整或者检查对象生命周期是否过长。
4. GC垃圾回收与内存分配策略
4.1 判断对象是否存活?
(1)引用计数法:给对象增加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,当引用失败时,计数器值就减1;任何时刻计数器的值为0的对象就是不能再被使用的对象,即就是该对象“已死”。该方法简单,判定效率也比较高,大部分情况下是一个不错的算法,但是在主流的JVM中并没有采用引用计数法管理内存,问题在于:引用计数法无法解决对象的循环引用问题。
(2)可达性分析算法:通过一系列称为“GC Roots”
的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为“引用链”,当一个对象到GC Roots
没有任何引用链相连时,即就是GC Roots
到该对象不可达时,证明该对象是不可用的,即该对象“已死”。
GC Roots:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象,即本地方法栈、虚拟机栈中的变量,类中的常量与静态变量。
扩充:JDK1.2之后对引用的扩充
(1)强引用:如:Person person = new Person()
,只要出现new
就是强引用,无论是否发生内存溢出OOM
,都不会回收被强引用指向的对象;
(2)软引用:softReference
描述软引用,若对象只被软引用指向,当内存够用时,不回收此对象;当内存不够用时,即抛出OOM
异常时,会回收掉所有仅被软引用指向的对象;软引用的对象为有用但不必须的对象,如:缓存对象;
(3)弱引用:weakReference
描述弱引用,若对象只被弱引用指向,当下次GC开始时,无论内存是否够用,都会回收掉只被弱引用指向的对象;
(4)虚引用:PhatomReference
描述虚引用,又称为幽灵引用或幻影引用。虚引用不会对生存周期产生任何影响,也无法通过虚引用取得一个对象,设置虚引用的目的在于能在该对象被GC之前收到一个系统信息,仅此而已。
4.2 缓刑阶段,finalize()
在可达性分析算法中不可达的对象并非“非死不可“,要宣告一个对象真正的”死亡“,必须经过两次标记:不可达的对象会被第一次进行标记并且进行一次筛选,看此对象是否有必要执行finalize()
方法。当前对象没有覆盖finalize()
方法或者finalize()
方法已经被JVM调用过,则虚拟机认为没有必要执行,此时的对象才是真正的”死亡“。任何一个对象的finalize()
方法都只会被系统自动调用一次。
4.3 如何进行垃圾回收?GC算法?
4.3.1 回收方法区
方法区的回收主要是两部分内容:废弃常量和无用的类。回收废弃常量类似于回收Java堆中的对象。判断是否是无用的类,三个条件:(1)该类的所有实例都已经被回收,即在堆上不存在任何该类的实例;(2)加载该类的ClassLoader类加载器已经被回收;(3)该类对应的Class对象没有在任何其它地方被引用,无法在任何地方通过反射访问该类的方法。同时满足以上三个条件的类可以被回收,而不是必然被回收。
4.3.2 垃圾回收算法
堆分为新生代和老年代。新生代的对象存活率比较低,老年代对象存活率高。分代收集:新生代采用复制算法,老年代采用标记清除算法。
现在的商用虚拟机:将新生代分为一块较大的Eden
区和两个较小的Survivor(From和To)
区,Eden : From : To = 8 : 1 : 1
,每次使用Eden
和Survivor
中的一块。当回收时,将Eden
和Survivor
中还存活的对象一次性复制到另一块Survivor
上,最后清理掉Eden
和刚才用过的Survivor
区域。具体步骤:step1:当Eden
区第一次满时,触发第一次GC,将Eden
区中存活的对象复制到From
区;step2:当Eden
区第二次满时,将Eden
区和From
区中存活的所有对象复制到To
区,一次性清理掉Eden和From
;step3:同step2,将Eden和To
区复制到From
区,清理Eden和To
区,如此From和To
来回的换,但是若干对象会在From和To
来回复制,默认复制15次,会将此对象移动到老年代。规定当From和To区都放不下该对象时,直接将该对象放到老年代。有一种空间分配策略就是大对象直接进入老年代,长期存活的对象将进入老年代,当老年代空间不够时,才会触发老年代的垃圾回收,而这种概率是极低的。
空间分配担保:在发生新生代垃圾回收之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于,说明此次新生代垃圾回收是安全的;如果小于,则虚拟机会查看一个HandlePromotionFailure
的设置值是否允许担保失败。如果值为true,就会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次新生代垃圾回收,但是此次回收仍然是有风险的,因为新生代采用复制算法,若是大量对象在垃圾回收之后仍然存活,而Survivor
区又比较小,此时就需要老年代进行分配担保,而进行担保的前提是老年代有足够的空间去容纳这些对象,然而一共有多少对象在回收之后会存活是不可知的,因此取之前每次垃圾回收后晋升到老年代对象大小的平均值作为参考,而取平均值也是一个概率性事件,若是某一次新生代垃圾回收之后存活的对象突然增加,远高于平均值,那么必然导致担保失败,此时只能在失败之后重新进行一次老年代垃圾回收,但这种失败的概率是很低的,大部分情况下都可以担保成功,这样一来就避免了频繁的进行老年代垃圾回收;如果小于或者值为false,则进行一次老年代垃圾回收。
(1)标记-清除算法
思路:先标记出所有需要回收的对象,在标记完成之后统一清除被标记的对象。
问题:效率问题:标记和清除这两个过程效率都不高;空间问题:标记清除后会产生大量不连续的内存碎片,碎片太多会导致在以后程序运行过程中需要较大空间时,无法找到足够连续空间而不得不提前触发一次垃圾回收。
(2)复制算法(新生代回收算法)
思路:为了解决标记清除的效率问题。将可用内存按容量均分两块,每次使用其中一块,当这块内存需要进行垃圾回收时,会将此区域内存活的对象复制到另一块区域,然后一次性清理掉已经使用过的内存区域。
优点:每次都是对半个区域进行回收,无需考虑碎片问题,实现简单,效率高。
(3)标记-整理算法(老年代回收算法)
思路:复制算法在对象存活率比较高时会进行较多的复制操作,所以效率会变得很低。而老年代对象存活率都比较高,因此在老年代一般不使用复制算法,而采用标记整理算法,过程类似于标记清除,只是先标记,后整理,把所有存活对象都向一端移动,然后再直接清理掉端边界之外的内存区域。
(4)分代收集算法
当前的JVM就采用分代收集算法,根据对象存活周期将内存划分为不同的区域,然后决定采用复制算法(新生代)还是标记清除或标记整理(老年代)算法。
(5)问题:Minor GC 和 Full GC是什么?有啥区别?
Minor GC:又称为新生代GC,指的是发生在新生代的垃圾收集,因为Java对象大多具备“朝生夕灭”的特点,所以Minor GC
采用复制算法,而且比较频繁,一般回收速度也比较快;
Full GC:又称为老年代GC或者Major GC
,指的是发生在老年代的垃圾收集,出现了Full GC
经常会伴随至少一次的Minor GC
,但也并非绝对。Major GC
的速度一般会比Minor GC
慢10倍以上,发生的频率也比较低。
5. JDK内置的JVM工具(bin目录下)
5.1 jps
PID + grep / jps -l
:显示当前操作系统内所有的JVM进程,返回进程ID,其中-l
可以输出主类的全名,即:包名.类名。
5.2 jstat
用来收集HotSpot
虚拟机各方面的运行数据,即:监视JVM各种运行状态信息的命令行工具。可以显示本地或者远程JVM中的类加载(jstat -class)
、内存、垃圾回收(jstat -gc)
等运行数据。
5.3 jinfo
显示和调整HotSpot
虚拟机配置信息,格式为:jinfo [option] pid
。
5.4 jmap
内存映射工具,生成虚拟机的内存转储快照,即:生成heapdump
文件,查看指定JVM的内存(堆、执行队列、永久代等)情况,格式为:jmap [option] vmid
。
5.5 jhat
与jmap
搭配使用,分析heapdump
文件,建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果。
5.6 jstack
jstack pid -l
:查看指定JVM进程的线程堆栈情况(一般多线程卡死使用此工具定位问题)。
6. Java内存模型JMM
6.1 目的: JVM定义了一致Java内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现Java程序在各平台下都能达到一致的内存访问效果。
6.2 规则: JMM主要定义了各个变量的访问规则,包括实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数,因为是私有的,不会被线程共享。
6.3 规定: JMM规定了所有变量都存储在主内存中,每个线程还有自己的工作内存(保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作在工作内存中进行),不同线程之间无法直接访问对方工作内存中的变量,需要通过主内存来完成。
6.4 内存间交互:主内存与工作内存之间交互的8个操作,每个操作必须是原子的且不可分割的。
(1) lock
(锁定) :作用于主内存,把一个变量标识为一个线程独占的状态;
(2) unlock
(解锁): 作用于主内存,释放锁定的状态;
(3) read
(读):作用于主内存,把变量值传输到线程的工作内存,以便后续操作;
(4) load
(载入):作用于工作内存,把read
操作得到的值放入工作内存的变量副本中;
(5) use
(用): 作用于工作内存,把变量值传递给执行引擎;
(6) assign
(赋值):作用于工作内存,把执行引擎接收到的值赋给工作内存的变量 ;
(7) store
(存储):作用于工作内存,把变量值传递给主内存,以便后续操作;
(8) write
(写):作用于主内存,把store
操作得到的值放入主内存变量中。
6.5 JMM三大特性
(1) 原子性:大致认为,基本数据类型的操作都是原子的,若需大范围的原子性,需使用synchronized
关键字约束,即:一个操作或多个操作要么全执行且中间不会被打断,要么都不执行;
(2) 可见性:指当前线程修改了共享变量的值,其它线程应该立即得知这个修改,使用synchronized,volatile,final
三个关键字来保证可见性;
(3) 有序性:在本线程内观察所有操作都是有序的,指的是线程内表现为串行;在一个线程中观察另外一个线程,所有的操作都是无序的,指的是指令重排和工作内存与主内存同步延迟现象。JMM具备一些天然有序性,不需要通过任何手段就可以保证有序性,也称为happens-before
原则,如:一个线程内,按照代码顺序,书写在前面的先行发生于书写在后面的,一个unlock
操作先行发生于后面同一个锁的lock
操作,对一个变量的写先行发生于后面变量的读操作,A先行于B发生,B先行于C发生,则A先行于C发生,一个对象的初始化完成先行于它的finalized()方法等等。若两个操作执行次序无法从happens-before
原则推导出来,就不能保证其有序性,虚拟机可以对其重排。
6.6 volatile变量
(1) 保证变量对所有线程的可见性。volatile
在各个线程中是一致的,但是volatile
的运算在并发下仍然是不安全的,因为Java中的运算并非原子操作,如自增操作,volatile
保证在取值时是正确的,但是后续会有其他线程的修改操作,最后把变化之前的值同步到了主内存中。
(2) 内存屏障,禁止指令重排。当程序执行到volatile
变量的读写操作时,保证了在其前面的操作都已经执行完毕,且结果对后续操作可见,且后续操作还没有进行;在JVM进行指令优化时,不会将volatile
变量的语句提前或者滞后执行。