1、概述
我们实际的开发中,先利用JDK(调用本地的API)开发属于自己的JAVA程序后,通过JDK中的编译程序(javac)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些JAVA字节码,JVM解释这些字节码,映射到CPU指令集或OS的系统调用。所以在讲JVM之前,我们先来讲一下容易和JVM搞混的两个概念JDK和JRE。简单粗暴的理解就是:JDK包含JRE,JRE包含JVM
JVM:Java虚拟机,它只认识xxx.class这种类型的文件,它能够将class文件中的字节指令进行识别,并调用操作系统上API完成动作。所以说,jvm是Java能够跨平台的核心。
JRE:Java运行时环境,jre包括jvm和java核心类库与支持文件。
JDK:Java开发工具包,jdk是整个java开发的核心,包括了java运行环境(jre)、一堆java工具(javac、java、jdb等)和java基础类库(即Java API包括rt.jar)。
2、JVM基本概念
2.1 什么是JVM
jvm中文名称叫做Java虚拟机,它是由软件技术模拟出来计算机运行的一个虚拟的计算机。
jvm也充当这个一个翻译官的角色,我们编写出java程序后,是不能直接被操作系统所识别的,这时候jvm的作用就体现出来了,它负责把我们的程序翻译给操作系统听,告诉它我们的系统需要做什么操作。
我们都知道Java的程序需要经过编译后,产生.Class文件,JVM才能识别并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因。
2.2、JVM基本原理介绍
jvm体系总体分四大块:
- 类的加载机制
- jvm内存结构
- GC算法 垃圾回收
- GC分析 命令调优
3、JVM运行时数据区
运行的程序是内容是放在运行时数据区中的,如上图蓝色那块依次来说明一下:
3.1 堆
保存所有引用类型的真是信息(线程共享),也是说那些new出来的对象都是放在这块区域的。
3.2 虚拟机栈
虚拟机栈:线程私有,生命周期和线程一致。主要用来存储方法运行过程中一些临时变量,是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。当一个方法运行到一半需要调用另一个方法时,就创建一个新的栈帧表示新调用的方法,将原来那个方法压入栈中。当方法运行完毕,栈帧出栈,原来方法处于栈顶接着运行。和栈这一数据结构一样,虚拟机栈里面的栈帧遵循后进先出的原则。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
3.3 方法区
3.4 程序计数器
public class TestDemo{ public static void main(String args[]){ String str = null; str.length(); } }
上面程序会报空指针异常,如下图,在报的这个异常中,有一行日志 at TestDemo,main(TestDemo.java:4) 代表程序运行到TestDemo 中main()函数第四行的时候发生的错误,就是通过程序计数器来记录这个程序运行的位置的。
3.5 本地方法栈
和虚拟机栈类似,不过本地方法栈里面运行的方法不是用java写的,一般是用c或c++写的为虚拟机使用到Native方法服务,也有类似栈帧的的概念。
用一张图描绘每个区域存储的内容
4、内存模型和垃圾回收
正式阅读之前需要了解相关概念:Java 堆内存分为新生代和老年代,新生代中又分为1个 Eden 区域 和 2个 Survivor 区域。
4.1 概述
jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。
4.2 GC的对象
需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活有两种方法:引用计数和可达性分析
引用计数:每一个对象都有一个引用计数属性,新增一个引用时计数增加1,释放引用时计数减1。计数为0时可以回收。此方法简单,但无法解决对象互相引用的问题。
从上图中可以看出,如果不小心把Obj1-reference 和 Obj2-reference置null。则在java堆中的两块内存依然保持互相引用无法回收。
可达性分析: 从GC Root开始向下搜索,搜索所走过的路径称之为引用链。当一个对象到GC Root没有任何引用链相连时,则证明此对象不可用,不可达对象。
可作为 GC Roots 的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
4.3 什么时候触发GC
1.程序调用System.gc时可触发
2.系统自身决定触发GC的时机(根据Eden和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)
GC又分为Minor gc和Full gc
Minor gc:当Eden区满是触发,Minor gc
Full gc:
a.调用System.gc时,系统建议执行Full GC,但是不必然执行
b.老年代空间不足
c.方法区空间不足
d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
GC主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。(回收方式即回收算法详见后文)
4.3.1 回收方法区:
在堆中,尤其是在年轻代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。永久代垃圾回收主要两部分内容:废弃的常量和无用的类。
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足
- 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法
4.4 GC算法
GC常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。
目前主流的JVM(HotSpot)采用的是分代收集算法。
分代收集算法
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
具体过程:新生代(Young)分为Eden区,From区与To区
当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区。
这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,
再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(阈值),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。
老年代经过这么几次折腾,也就扛不住了(空间被用完),好,那就来次集体大扫除(Full GC),也就是全量回收。如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。
参考:
https://blog.csdn.net/csdn_aiyang/article/details/72876272
https://blog.csdn.net/laomo_bible/article/details/83112622
https://blog.csdn.net/qq_41701956/article/details/81664921