Hello!各位优秀的程序员小伙伴们,欢迎来到这篇博客来了解JVM,本篇文章主要对于java虚拟机中的内存模型,OOM,类加载的机制,以及一些常见的垃圾回收算法和垃圾回收器做相关介绍,点击左侧的目录可实现对你感兴趣的地方进行快速访问哦!
举个例子:创建一个java类,在idea上写好后进行main方法编译运行到出来这个编译的结果。在这个过程中存在jvm,那么jvm到底是什么呢?且看下面这个图来解释:
创建一个java类编译后启动的一个java进程,在这时系统为进程分配了一块内存空间,执行进程的代码指令:其中就会创建java虚拟机(在java虚拟机中启动了一个 线程,来执行main方法,执行的方式是java虚拟机把class字节码的内容翻译为所在系统的机器码)
所以JVM就是:把Class字节码翻译成机器码(计算机能够识别)的一款Java底层工具
我们经常提及的JDK和Jre和Jvm都是什么关系呢?且看下图
这是文件中存储的关系图:
所以jdk中包含了jre,jre中包含了jvm
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Native Interface)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
jvm运行时数据区如下图所示:
接下来对每个部分的功能进行简单的介绍:
方法区:
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
堆区:
程序中创建所有的对象都存放在堆中。堆中分为两个区域:新生代和老生代区。新生代就是新建的对象,当新生代经过多次GC(垃圾回收)就会放入老生代区。
虚拟机栈:
Java 虚拟机栈的生命周期和线程相同(一个线程对应一个栈帧),Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
其中存储的相关数据的描述如下:
局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变量。
操作数栈:每个方法会生成一个先进后出的操作栈。
动态链接:指向运行时常量池的方法引用。
方法返回地址:PC 寄存器的地址
查看IDEA中栈帧的情况,创一个如图方法:
打断点运行查看数据信息,
运行到打断点的位置,
运行完之后发现输出数据并没有改变,是因为swap中的方法中的m和n是局部变量,从main方法中传入了m和n的值,在swap方法中是已经改变后的结果,但是在mian方法中没有去接收该结果,所以main方法中的数值和swap方法中的数值不一样。
堆溢出简称为OOM,OOM全称是OutOfMemoryError(内存不足异常),那么为什么会出现内存不足的情况呢?主要原因有两点,内存泄漏和内存溢出。对于内存溢出和内存泄漏介绍如下所示:
内存溢出:内存对象确实还应该存活,某个运行时数据区域,需要创建数据,但是空间不足,就会出现内存溢出。
内存泄漏:在那个内存区域中,保存了一些对象等数据,但是以后不会用到,也没有办法被GC,就会出现内存泄漏。举个例子,用户登录页面之后,不做退出操作,数据仍被保留,非常多的用户不做退出操作就会导致出现内存泄漏。
常见的虚拟机栈的异常为StackOverFlow异常和OOM异常。
对于该两类的异常解释为:
StackOverFlow:栈帧调用太深导致。创建一个方法不断的递归调用,就会在线程中不断的创建栈帧,最后出现该异常。
OOM:如果虚拟机在拓展栈时无法申请足够的内存空间,就会抛出OOM异常。
1.在java类中执行mian方法时,需要先执行类加载,;
2.运行时,执行静态方法调用,静态变量操作时进行加载
3.new一个对象的时候,进行类加载
4.通过反射创建一个类的对象,就可以再通过反射生成实例对象,或者调用静态方法类加载只执行一次(已经执行类加载,方法区就已经有了类的信息,堆也有的类对象)如果在多线程中,有需要执行类加载的代码(上面几种时机)jvm执行类加载的时候,会进行synchronized加锁来保证线程安全
类加载的过程主要分为三部分:加载、连接、初始化
加载:
加载class字节码到方法区,在堆中,生成一个class类对象
(连接操作)验证:
验证class字节码数据,是否安全并且符合java虚拟机规范
(连接操作)准备:
静态变量设置为初始值(对象初始值就是null,基础数据类型,就是对应的初始值)常量(final修饰的)会设置为真实的值
(连接操作)解析:
将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程
符号引用:编译的class文件中 需要有 变量/引用 到 值 对应的关系 此时还没有加载到到内存中就使用“符号引用”来表示这种关系
直接引用:执行类加载,把calss字节码加载到内存中,内存中体现的 变量到值的关系,称为直接引用
初始化
静态变量真正的初始化赋值,静态代买块初始化
双亲委派模型(jdk默认的加载机制):不直接执行当前类加载器的类加载代码,而是要查找当前类加载器的父类加载器,父类也是找父类,直到最顶级的再进行加载,找不到就交给下一类。
总的来说:从下到上查找类加载器,从上到下执行类加载
优点:使用双亲委派机制,保证加载的先是jdk提供的类,避免了重复加载类。在创建同jdk类中同名的类时,会避免使用本地创建的类来执行类加载,保证了编译的安全。
缺点:扩展性相对不好,如JDBC操作,不通过数据库的驱动包就不一样,而jdk对于不同的驱动包不能做的自主识别,所有不能对于所有的类加载完全实现。
解决方法:采用SPI机制,将需要加载的类的全限定名 放在一个 jdk 能够找到的位置,然后告诉jdk,让jdk在执行类加载的时候找到该位置执行加载。
引用计数算法:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死“。
可达性分析算法:
使用“引 用”来判断死亡对象:将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。对于每种引用的介绍如下:
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
但是存在两个缺点:
1.效率较低
2.清除元素之后会产生大量的不连续的内存碎片
"复制"算法是为了解决"标记-清理"的效率问题。把所用的内存划分为两个一样大小空间,每次都只使用其中一块。把存活的对象复制到另一个空间,就把之前的空间清理了。
好处:清空半区的时候效率高,不会出现内存碎片
缺点:利用率较低(只有50%)
类似标记清除算法,采取的方案,是将存活的对象,移动到连续的空间,再清理剩余空间
优点:不会出现内存碎片问题
在堆中,根据对象创建,回收的特点,分为了两块区域:
(1)新生代
新生代又分为:Eden(E区),2个Survivor(S区)对象朝生夕死:很快的创建,又很快的变为不可用的垃圾
其中默认的划分E:S:S=8:1:1,空间利用率就是90%。默认就是每次使用E区和一块S区来保存对象,另一个S区留空。在进行Gc的时候,把存货对方复制到另一个留空的S区。
采取的算法:复制算法
(2)老年代
对象可能长期存活,
采取的算法:标记清除算法,标记整理算法
Serial 收集器是最基本、发展历史最悠久的收集器。
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。
ParNew收集器其实就是Serial收集器的多线程版本。
除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集
器。
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
G1(Garbage First)垃圾回收器是用在堆的空间很大的情况下,把堆划分为很多相等的区域,每个区域根据需要设置E(Eden)、S(Survivor)、T(Tenured 老年区)。然后并行的对其进行垃圾回收。G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
新生代:
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代:
对于老年代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:
初始标记(Initial Mark)阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。
并发标记(Concurrent Mark)阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,就是如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。
最终标记(CMS中的Remark阶段) - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。
筛选回收(Clean up/Copy)阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的
以上就是今天带来的JVM相关的知识了,感谢观看呦!!!