JVM虚拟机
一、JVM概述
JVM(Java Virtual Machine)是Java虚拟机的缩写,它是在物理计算机上模拟的计算机,用于执行Java字节码指令。JVM帮我们屏蔽了操作系统的差异,具体跨平台性,一次编写,到处运行;JVM的垃圾回收机制可以实现自动内存管理,大大减轻了程序员的负担。
二、JVM的组成和运行流程
2.1 程序计数器
程序计数器(Program Counter,PC):用于记录线程执行的字节码指令的地址,相当于记录了线程执行到了哪一行字节码。每个线程都有自己的程序计数器,这意味着每个线程在执行Java代码时都有自己独立的程序计数器
2.2 堆
线程共享的区域,主要用来保存对象实例,数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则会抛出 OutOfMemoryError 异常(内存溢出)
年轻代被划分为三部分,Eden 区(Eden 区主要存放新创建的对象)和两个大小严格相同的 Survivor 区根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到老年代区间
老年代主要保存生命周期长的对象,一般是一些老的对象
元空间保存主要保存类信息、静态变量、常量、编译后的代码
java1.7中堆中有方法区(永久区)在1.8中将其存入了本地内存的元空间中
2.3 虚拟机栈
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的。生命周期和线程一致,作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。栈的特点:栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对Java栈的操作只有两个:
1、每个方法执行,伴随着进栈(入栈、压栈);
2、执行结束后的出栈工作。对于栈来说不存在垃圾回收(但栈存在溢出的情况)方法结束,栈回收
2.3.1 栈溢出
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。HotSpot
虚拟机不支持栈动态扩展,所以只有在创建线程申请内存时因为无法获得足够的内存才会导致 OutOfMemoryError异常。
我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
2.4 JVM运行过程
两个子系统为 类装载子系统 ClassLoader,执行引擎子系统 Execution engine;
类加载子系统:包含类加载器;根据给定的全限定类名装在class文件到运行时数据区的方法区;执行引擎:包含即时编译器(JITCompiler)和垃圾回收器(Garbage Collector);执行class文件中的命令;
两个组件为 运行时数据区 Runtime data Area, 是jvm的内存;包含方法区,虚拟机栈,本地方法栈,堆,程序计数器;
本地接口 Native Interface 与本地方法库交互,与其他变成语言交互的接口;
程序执行之前需要先把 java 代码转换成字节码( class 文件), JVM 首先需要把字节码通过一定的方式 类加载器( ClassLoader ) 把文件加载到内存中 运行时数据区。字节码文件不能直接被底层操作系统执行,它是 JVM 的一套指令集规范,所以需要特定的命令解析器执行引擎将它翻译成底层系统指令再由cpu执行,而这个过程中需要调用其他语言的接口本地库接口来实现整个程序的功能
三、垃圾回收机制
3.1 强引用、软引用、弱引用、虚引用的区别
强引用(Strong Reference)
最普遍的引用:Object obj = new Object()
抛出OutOfMemoryError种植程序也不会回收具有强引用的对象
通过将对象设置为null来弱化引用,使其被回收
软引用(Soft Reference) 对象处于有用但非必须的状态 只有当内存空间不足时,GC会回收该引用的对象的内存,可以用来实现高速缓存
弱引用(Weak Reference) 非必须的对象,比软引用更弱一些 GC时会被回收, 被回收概率也不大,因为GC线程优先级比较低
虚引用(PhantomReference) 不会决定的对象的生命周期
(就像没有任何引用一样)任何时候都可能被垃圾回收器回收 主要用来跟踪对象 垃圾回收器回收的活动,起哨兵作用
但是必须先跟引用队列(ReferenceQueue)联合使用 引用队列(ReferenceQueue)无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达 储存关联的且被GC的软引用,弱引用以及虚引用
3.2 java回收机制
Java的垃圾收集(Garbage Collection, GC)是Java虚拟机提供的一种自动管理机制,用于回收和再分配无用对象占用的内存。
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉GC。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收机制,换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。
垃圾回收主要是针对堆中的内存,如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
定位垃圾有两种方式,第一个是引用计数法,第二个是可达性分析算法
3.2.1 引用计数法
一个对象被引用了一次,在该对象上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收。
优点:实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OOM错误。 区域性,
更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:每次对象被引用时,都需要去更新计数器,有一点时间开销。
浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
致命缺陷 :无法解决循环引用问题,会引发内存泄露。
3.2.2 可达性分析算法
目前主流的商用JVM都是通过可达性分析来判断对象是否可以被回收的。
通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC Roots没有任何引用链相连,就说明该对象不可达,即可以被回收。
对象可达指的就是:双方存在直接或间接的引用关系。
根可达或GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。
垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。
然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收
GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。
只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛。
当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。
即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。
GC Roots一般包括全局对象(方法区静态属性引用的对象、方法区常量池引用的对象)和执行上下文(方法栈中栈帧本地变量表引用的对象、JNI本地方法栈中引用的对象、被同步锁持有的对象)
3.3 垃圾回收算法
3.3.1 标记清除算法
标记清除算法将垃圾回收分为2个阶段,分别是标记和清除
1、根据可达性分析算法得出的垃圾进行标记
2、对这些标记为可回收的内容进行垃圾回收
优点:标记和清除速度较快
缺点:碎片化较为严重,内存不连贯
3.3.2 复制算法
复制法的主要思路就是将存活的对象复制到另一块内存区域中,然后清空原来的内存区域,复制的过程中就解决了碎片的整理过程
一般年轻代的垃圾回收算法采用的就是复制法 优点:在垃圾对象较多的情况下,效率较高,而且垃圾清理后没有内存碎片 缺点:将内存空间一分为二,但
2 块内存空间在同一个时刻,只能使用一个,内存使用率较低
3.3.3 标记整理算法
标记整理法与标记清除法类似,但标记整理法解决了标记清除算法的碎片化的问题
因为标记整理法比标记清除法多了一步,移动存活对象在内存中的位置(将存活的对象都向内存的某一端移动),当然,这一步对效率有一定的影响
很多老年代的垃圾回收算法都是采用标记整理法
3.4 垃圾回收器
3.4.1 串行垃圾收集器
Serial 和 Serial Old 串行垃圾收集器,是指使用单线程进行垃圾回收,适合堆内存较小的情况(个人电脑),在企业开发中很少用
Serial 作用于新生代,采用复制算法 Serial Old 作用于老年代,采用标记-整理算法
串行垃圾回收器在进行垃圾回收时,只有一个线程在工作,并且 Java
应用中除了垃圾回收线程以外的所有线程都要暂停(STW),等待垃圾回收的完成
3.4.2 并行垃圾收集器
Parallel New 和 Parallel Old 是一个并行垃圾回收器,JDK8 默认使用此垃圾回收器
Parallel New 作用于新生代,采用复制算法 Parallel Old 作用于老年代,采用标记-整理算法
并行垃圾收集器在进行垃圾回收时,多个线程在工作,并且 Java 应用中除了垃圾回收线程以外的所有线程都要暂停(STW),等待垃圾回收的完成
3.4.3 CMS(并发)垃圾收集器
CMS,全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好,其最大特点是在进行垃圾回收时,应用仍然能正常运行
3.4.4 G1垃圾收集器
G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。
它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
它的设计目标是为了适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。
G1作为主要面向服务端应用的垃圾收集器,HotSpot开发团队赋予的期望是在为未来可以替换掉JDK5中发布的CMS收集器。
G1 把堆内存分割为很多不相关的区域(Region,物理上不连续区分,逻辑上是连续区分 Eden 区、Survivor区,old区),Region 作为单次回收的最小单元,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC模式。基于 Region 的堆内存布局是它能够实现这个目标的关键,GC 从传统的连续堆内存布局逐渐走向了不连续内存块布局(物理上不连续区分 Eden 区、Survivor区,old区) 每个区域都可以充当eden 、survivor,old,humongous 其中 humongous 专为大对象准备
应用于新生代和老年代,在JDK9之后默认使用G1
响应时间与吞吐量兼顾
分成三个阶段:新生代回收、并发标记、混合收集
如果并发失败(即回收速度赶不上创建新对象速度)会触发 FuIl GC