目录
一.JVM的概念
什么是JVM?
JVM是JavaVirtualMachine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
二.JVM的运行流程
1.class文件如何被JVM加载并运行
①当.java文件被编译为.class文件时,.class文件会被加载到类加载子系统,然后由类加载子系统将文件加载到运行时数据区
②在运行时数据区中,类对象被加载到方法区中,便于后面new出来的实例对象可以通过这个类对象模板中创建新的对象。
③创建出来实例对象会被加载到堆中
④虚拟机栈:每个线程都会在虚拟机栈中开辟一个空间,每调用一个方法时,这个方法就会被压入栈中,也就是说每个栈中存放的是方法调用的层级
⑤本地方法栈:同样是每一个线程都会在本地方法栈中开辟一块内存,每次调用一个本地方法,这个本地方法就会被加载到本地方法栈中。
⑥程序计数器:记录当前线程所运行到的指令地址:由于考虑到java虚拟机在多线程模式下是通过线程轮流切换并分配时间片的方式进行的,因此当某个线程分配的时间片使用完但是当前线程并没有执行结束时,这时就需要使用程序计数器记录下当前线程所运行到的指令地址,当当前线程再度被分配到时间片时,从当前指令下继续执行。
2.JVM运行时数据包括哪些区域(M)
①方法区:在JDK1.7之前, 方法区又被称作永久代,在1.8及之后被称为元空间,区别在于实现方式的不同,方法区是当类对象被加载到JVM时存储的地方,类对象被存储到方法区中以便后续需要创建实例对象时直接从方法区中的类对象获取并创建实例对象。需要说明的是,方法区是所有线程所共享的
②堆:创建的实例对象都会被加载到堆空间中,堆空间的大小可以通过JVM中的参数进行设置:Xms10(最小堆内存空间) 是设置堆空间大小,Xmx10(最大堆内存空间)也是设置堆空间大小的:通常我们将这两个内存参数设置为同一个大小,我们一般将两个参数均设置为线程运行可能消耗的最大堆空间,如果内存空间比较小时,可能会出现OOM错误(OUT OF MEMORY ERROR),一旦出现这个错误,我们可以将堆空间设置的大一点。
③java虚拟机栈:每个线程都会在虚拟机栈中开辟一个空间,每调用一个方法时,这个方法就会被压入栈中,也就是说每个栈中存放的是方法调用的层级,虚拟机栈容量的大小一般由Xss这个参数确定,如果栈溢出,会报出StackOverFlow错误
④本地方法栈:同样是每一个线程都会在本地方法栈中开辟一块内存,每次调用一个本地方法,这个本地方法就会被加载到本地方法栈中。
⑤程序计数器:记录当前线程所运行到的指令地址:由于考虑到java虚拟机在多线程模式下是通过线程轮流切换并分配时间片的方式进行的,因此当某个线程分配的时间片使用完但是当前线程并没有执行结束时,这时就需要使用程序计数器记录下当前线程所运行到的指令地址,当当前线程再度被分配到时间片时,从当前指令下继续执行。
三.类加载的过程(M)
1.加载
将所有的.class文件全部加载到虚拟机中
2.验证
根据.class文件的规范对当前的文件进行验证
3.准备
将各种类型的值初始化为默认的值,int 初始化为0,float初始化为0.0f.......
4.解析
将java虚拟机中常量池的符号引用替换为直接引用,也就是初始化常量池的过程。(例如:原本常量池中的字符串并没有直接引用,(还是占位符),在这个过程中将创建一个新的字符串并将常量池中的占位符进行替换)
5. 初始化
之前的工作都是准备工作,这一步是真正执行类中的java代码,经过这一步,真正的java对象才会被创建出来
四.双亲委派模型
1.双亲委派模型分析
不同的类在加载时会使用不同的类加载器:
其类加载器的策略如下:当创建一个类时,先从applicationClassLoader开始向上转发,一直到BootStrapClassLoader,BootStrapClassLoader在自己的加载路径中查找是否存在这个类,有则加载,没有则向下转发到ExtClassLoader,ExtClassLoader在自己的加载路径中查找有没有这个类,有则加载,没有则向下转发到ApplicationClassLoader,在自己的路径中查找并加载这个类
2.JAVA中有哪些类加载器(M)
五.垃圾回收机制
1.死亡对象的标识
①引用计数算法
概念:当对象被引用,那么其引用的次数就加1,当引用的对象为0的时候,就将这个对象标识为死亡对象,这个对象就该被回收了。
图示如下:
过程分析:当创建两个对象,定义两个变量指向创建的两个对象,此时这两个对象的引用都+1,同时使用对象的instance属性也指向这两个创建的对象,这时这两个新创建的对象的引用次数都是2了,但是如果对这两个变量置为空,则两个新创建的对象的引用次数-1,但是由于两个变量都为空了,两个变量的instance属性也无法访问到这两个对象了,但是由于这两个对象的引用次数并没有变为0(instance属性仍然指向两个对象),因此这两个对象无法进行回收,这也就造成了内存泄漏的问题
notes:内存泄漏:内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏问题在程序刚开始运行时并不容易发现,当程序运行了一段时间,堆空间不断放入新的对象而无法及时清除,最终会导致堆空间被占满,导致程序崩溃。
②可达性分析算法
可达性分析算法的实现原理如下:从GC Roots(起始结点集)根据引用关系从上往下搜寻,搜寻的路径称为【引用链】,如果某个结果到任何一个根对象都没有引用链,则称这个对象不可达,标识这个对象不可达。
在垃圾回收时首先会搜寻到所有的根结点【枚举根结点】,然后从这些根结点从上向下搜寻,这个过程需要暂停用户线程,即触发STW,能搜寻到说明这个对象保留,如果不能搜寻这个对象则使用垃圾回收的算法对其进行回收。
什么是GC Roots 呢?
GC Roots也是对象,而且是JVM一定不能回收的对象,在JVM进行GC的时候,在所有用户到达safepoint之后首先会进行STW,暂停所有用户的线程,之后进行对根结点的枚举,之后再从根结点从上往下搜寻。
GC Roots一般有以下几种类型:
1、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
2、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。
3、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。
4、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。
5、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁就失效了。
2.垃圾回收的算法
①标记清除算法
算法思路:标记当前所有对象中的不可用对象,在原位将其进行标注,之后进行清除
缺陷:但是这个算法的缺陷也比较明显:在原位将不可用对象进行了清除,会产生大量不连续的可用空间碎片,如果在这时需要创建较大的对象但是当前连续的可用空间不满足时,则需要触发下一次垃圾回收。
②标记复制算法
算法思路:将全部的可用空间划分为两部分,比如划分为空间1和空间2,空间1在清除完不可用对象后,存在大量的不连续的可用空间的碎片,空间2对空间1中的可用和不可用对象进行分类整理排列,在排序完成后将空间1进行整体清除,后面再次清除时重复这种操作
缺陷:这种算法的缺陷也相对明显,再每一次垃圾回收的过程中,只有一半的空间能真正发挥作用
③标记整理算法
算法思路如下:在每次清除完部分的不可用对象之后就对全部的对象进行排序整理
缺陷:每次清除都要进行排序整理,算法效率相对较低
垃圾回收的过程?(M)
不同的类对象进行了分代处理,将所有的类对象分为了新生代和老年代,其中新生代占堆空间的1/3,老年代占堆空间的2/3,新生代的空间中还有1/5的survivor(幸存者分为from和to两个区域)区域。对象进行了分代处理,垃圾回收也被划分为两种类型:①Minor GC(新生代的垃圾回收)②Full GC(Major GC/Full GC 发生在老年代的垃圾回收)。
1.Minor GC(新生代的垃圾回收)
新生代的垃圾回收采用的是复制算法(因为新生代中的对象迭代比较频繁,所以需要效率相对较高的算法,虽然复制算法所消耗的空间较大,但是其效率相对较高),其过程如下:
①创建对象时在Eden区直接创建,此时survivor两块区域都为空(空间大小之比:Eden:from:to=8:1:1)
②当我们再次创建对象,发现Eden区当前的容量不足以存放当前对象,则将Eden区的对象进行判断处理,对判定死亡的对象进行清除,存活的对象将其移至survivor区中的from区,移动到from区的对象的“年龄”+1;
③Eden区持续创建新的对象,并对没有引用的对象进行清除之后,Eden区存活的对象都被移动到了from区,Eden区和from区都被占用满了,这时要把Eden区和from区存活的对象移动到to区,然后将Eden区和from区都进行清空,在上一轮中被放置在from区的对象的年龄再加1
⑤在下一轮GC中会将from区和to区的位置互换,此时to区为空,from区和Eden区继续参与下一轮的GC ,重复上面的步骤
⑥经过不断的GC,在新生代中的对象的年龄达到了一定的阈值(默认阈值是15),这时候就要将这些对象移动到老年代
总结:我们纵观整个新生代垃圾处理的过程来看:在每一轮的GC中,新的对象会在Eden区创建,当Eden区剩余的容量不足以创建新的对象时,将所有的Eden区存活的对象移动到from区,在下一轮GC时会将from和Eden区存活的对象全部复制到to区,将from区和Eden区全部清空,将to区和from进行调换继续进行垃圾回收。
2.Full GC(老年代的垃圾回收)
老年代的垃圾回收策略并不是采用复制算法,而是标记整理算法, 因为进入老年代的对象相对于新生代而言并不会进行频繁的创建和销毁,所以使用标记整理算法比较适合。Full GC相对于Minor GC要慢很多,所以在JVM调优过程中,很大的一部分工作都是对Full GC的调节
3.垃圾回收器
介绍一下垃圾回收器(M)
在讲述垃圾回收器之前,我们先说一下垃圾回收器的发展过程:由于在垃圾回收的过程中要执行STW,所有的用户线程在这个过程中都要暂停线程服务,所以缩短STW的时间是垃圾回收器在完成业务需求之后所要追求的目标.。在刚开始时使用单线程垃圾回收器,随着程序规模的不断扩大和程序内容的不断丰富,单线程垃圾回收器无法满足用户需求,多线程垃圾回收器应运而生,随着程序的不断发展,多线程STW的时间也越来越长,又开始尝试新的垃圾回收器来缩短STW的时间
主要的垃圾回收器有以下几种:
- Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC
来强制指定。
Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本
ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现
Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC
来强制指定,用-XX:ParallelGCThreads=4
来指定线程数。
- Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。 - CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。