写在前文:参考大佬讲解,大部分都是参考大佬的,有加一点自己的理解和增加一些面试中遇到的JVM相关的问题,想了解的可以先看看大佬的再看看我写的查缺补漏
理解堆和栈
操作系统的堆和栈
操作系统的堆:一般由程序员分配释放,若程序员不释放,程序结束可能由OS回收,分配方式类似于链表
操作系统的栈:由操作系统自动分配释放,存放函数的参数值,局部变量等。操作方式于数据结构中的栈类似
JVM的堆和栈
问题一:为什么JVM的内存存放在操作系统的堆中?
答:操作系统的栈是操作系统管理的,它随时会被回收,所以如果JVM放在栈中,那java的一个null对象就很难确定会被谁回收了,所以GC的存在就一点意义都没有了。
其中左半部分并不是在JVM中,程序员编写的.java文件,经过JAVA编译器编译成.class文件(如maven工程需要maven install,打成jar包,jar包里都是.class文件),这些工作都是在编译器中进行的。
问题二:java被编译成了class文件,JVM怎么从硬盘上找到这个文件并装载到JVM里呢?
答:是通过java本地接口(JNI),找到class文件后并装载进JVM,然后找到main方法,最后执行。
问题三:JVM虚拟机位于操作系统的堆中,并且程序员写好的类加载到虚拟机执行的过程是?
(1)当一个classLoad启动的时候,classLoader的生存地点在jvm中的堆
(2)然后它会去主机硬盘上将A.class装载到JVM方法区
(3)执行引擎读取方法区的字节码自适应解析,边解析边运行(其中一种)
(4)方法区中的这个字节文件会被虚拟机拿来new A字节码()
(5)在堆内存生成了一个A字节码的对象
(6)A字节码的这个内存文件有两个引用:一个引用指向A的class对象,一个指向加载自己的classLoader
问题四:一个完整的程序在JVM运行的流程(注意与之前的区别,3是说的一个类)
(1)首先在一个程序启动之间,它的class会被类装载器装入方法区(Permanent区);
(2)执行引擎读取方法区的字节码自适应解析,边解析边运行(其中一种)
(3)然后PC寄存器指向了main函数的所在位置,JVM开始为main函数在java栈中预留一个栈帧,开始跑main函数
(4)main函数的代码被执行引擎映射成本地操作系统里相应的实现
(5)然后调用本地方法接口,本地方法运行的时候,操作系统会为本地方法分配本地方法栈,用来存储一些临时变量,然后运行本地方法,调用系统API等。
(6)如果方法区的内存空间不能满足内存分配需要时,将抛出.OutOfMemoryError异常
问题五:JVM虚拟机的生命周期
起点:当一个java应用main函数起动时虚拟机同时也被启动。先加载字节码文件到方法区,然后再找到main执行程序
PS:main函数就是一个java应用的入口,main函数被执行时,java虚拟机也就启动了,启动了几个main函数就启动了几个java应用,同时也启动了几个java虚拟机。
结束:只有当虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才结束生命。
JVM相关知识
JVM有两种线程
守护线程,如GC垃圾回收线程
PS:GC垃圾回收机制不是创建的变量为空就立刻回收,而是超出变量的作用域后被自动回收
非守护线程,即普通线程,如main函数就是一个非守护线程。只要有任何非守护线程还没有结束,java虚拟机的实例都不会退出。
如下图:
JVM结构图各模块
可以划分为数据区和功能区
数据区:即运行时数据区中的内容,其中方法区和堆是所有线程共享的,虚拟机栈、本地方法栈、程序计数器是线程私有的。
(一)程序计数器
1、程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
2、区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
3、当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
PS:因为本地方法存放在本地方法栈中,本地方法栈存放在缓存中,不通过PC寄存器,所以是undefined
4、程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
5、此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
(二)Java虚拟机栈
1、每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double;和reference(32 位以内的数据类型,具体根据JVM位数(64为还是32位)有关,因为一个solt(槽)占用32位的内存空间 )、部分的返回结果,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址;
JAVA虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令
2、栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进......F3栈帧,再弹出F2栈帧,再弹出F1栈帧。
理解:理解:比如main方法,执行main方法,创造一个栈帧,然后压入该class(由.java文件转换)产生的JAVA虚拟机栈中,然后main方法中调用A方法,入栈。如果A方法中没有调用其它方法,执行完毕出栈;如果A方法中有调用其它方法,则将调用的方法压入栈中(比如递归);
PS:JAVA虚拟机栈是有大小的,比如递归如果太深或者存在死循环,会造成栈内存溢出java.lang.StackOverflowError可以通过虚拟机参数-Xss来设置栈的大小
3、线程私有,它的生命周期与线程相同,每个线程都有一个。
4、执行return命令如果当前线程对应的栈中没有了栈帧,这个Java栈也将会被JVM撤销。
问题六:八大基本类型+String类型的存放位置
我们平时所说的八大基本类型的在栈中的存放位置是:运行时数据区->虚拟机栈->虚拟机栈的一个栈帧->栈帧中的局部变量表;
局部变量表存放的数据除了八大基本类型外,还可以存放一个局部变量表的容量的最小单位变量槽(slot)的大小,通常表示为reference;所以是可以放字符串类型的,但是要以 String a="aa";的形式出现,如果是new Object()那就只能是在堆中了,栈里面存的是栈执行堆的地址。
(三)本地方法栈
1、本地方法:jvm中的本地方法是指方法的修饰符是带有native的但是方法体不是用java代码写的一类方法,这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
2、作用同java虚拟机栈类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
3、是线程私有的,它的生命周期与线程相同,每个线程都有一个。
4、存储在缓存当中,调用速度非常快
(四)JAVA堆
1、堆是Java虚拟机所管理的内存中最大的一块,不同于上面3个,堆是jvm所有线程共享的。
2、在虚拟机启动的时候创建。
3、唯一的目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
4、Java堆是垃圾收集器管理的主要区域。因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
5、java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
6、如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
PS:堆内存大小-Xms -Xmx设置相同,因为-Xmx越大tomcat就有更多的内存可以使用,这就意味着JVM调用垃圾回收机制的频率就会减少(垃圾回收机制被调用是jvm内存不够时自动调用的)可以避免每次垃圾回收完成后JVM重新分配内存。
(五)方法区(也称作永久代,permanent区)
1、在虚拟机启动的时候创建,所有JVM线程共享,除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集。
2、用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据。
3、被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。
4、运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
5、指令集是个非常重要概念,因为程序员写的代码其实在jvm虚拟机中是被转成了一条条指令集执行的
左侧的foo代码是指令集,可见就是在方法区,程序计数器就不用说了,局部变量区位于虚拟机栈中,右侧最下方的求值栈(也就是操作数栈)我们从动图中明显可以看出存在栈顶这个关键词因此也是位于java虚拟机栈的。
指令是Java代码经过javac编译后得到的JVM指令,PC寄存器指向下一条该执行的指令地址,局部变量区存储函数运行中产生的局部变量,栈存储计算的中间结果和最后结果。
类加载是会先看方法区有没有已经加载过这个类,因此方法区中的类是唯一的。方法区中的类都是运行时的,都是正在使用的,是不能被GC的,所以可以理解成永久代。
问题七:方法区与堆的区别?
方法区存放了类的信息,有类的静态变量,final类型变量、field自动信息、方法信息,处理逻辑的指令集;
堆中存放的是对象(类的实例)和数组
可以理解为:方法区——类,堆——对象
问题八:方法区的内容是一次把一个工程的所有类信息都加载进去再去执行还是边加载边执行呢?
边加载边执行。如使用tomcat启动一个spring工程,启动过程会记载数据库信息,配置文件中的拦截器信息,service的注解信息,一些验证信息等,其中类信息就会率先加载到方法区。
如果我们想要程序启动的快一点,就会设置懒加载,把一些验证去掉,等到真正使用的时候再去加载。
PS:说明方法区的内容可以先加载进去,也可以在使用到的时候加载
问题九:方法区,栈、堆之间的过程
(1)类加载器加载的类信息放到方法区
(2)执行程序后,方法区的方法压入栈的栈顶
(3)栈执行压入栈顶的方法
(4)遇到new对象的情况就在堆中开辟这个类的实例空间
功能区:垃圾回收系统、类加载器、执行引擎
(一)类加载子系统
1、根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到Runtimedataarea中的methodarea(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Classloader。
2、对类的加载过程是:当一个classloader启动时,classloader的生存地点在jvm中的堆,然后它去主机硬盘上去装载A.class到jvm的methodarea(方法区),方法区中的这个字节文件会被虚拟机拿来new A字节码,然后在堆内存生成了一个A字节码的对象,然后A自己码这个内存文件有两个引用,一个指向A的class对象,一个指向加载自己的classloader。
双亲委派机制(JVM加载类时默认采用的机制)
通俗的讲,就是某个特定的类加载器在接到加载类的请求的时候,首先讲加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
举例:如JVM加载Test.class的时候
(1)首先会到自定义加载器中查找(其实是看运行时数据区的方法区有没有加载),看是否已经加载过,若已经加载过:返回字节码
(2)未加载过:则询问上一层加载器(AppClassLoader)是否已经加载Test.class
(3)未加载过:则询问上一层加载器(ExtClassLoader)是否已经加载Test.class
(4)未加载过:则询问上一层加载器(BoopStrap ClassLoader)是否已经加载Test.class
(5)如果BoopStrap ClassLoader还是没有记载过,则到自己指定类加载路径下("sun.boot. class.path")查看是否有Test.class字节码,有则返回没有则通知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看
(6)依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException
问题十:为什么采用双亲委派机制
1、类加载器代码本身也是java类,因此类加载器本身也是要被加载的,因此显然必须有第一个类加载器不是Java类,这就是bootStrap,是使用C++写的
2、虽然bootStrap、ExtClassLoader、AppClassLoader三个是父子类加载器关系,但是并没有使用继承,而是使用了组合关系
3、优点:具备了一种待优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,如jdk自带的几个jar包肯定是最顶级的。
(二)执行引擎(Executionengine子系统)
1、负责执行来自类加载器子系统(class loader subsystem)中被加载类中在方法区包含的指令集,通俗讲就是类加载器子系统把代码逻辑(什么时候该if,什么时候该相加,相减)都以指令的形式加载到了方法区,执行引擎就负责执行这些指令就行了。
2、程序在JVM主要执行的过程是执行引擎与运行时数据区不断交互的过程,可理解为上面“方法区中的动图”
3、但是执行引擎拿到的方法区中的指令还是人能够看懂的,这里执行引擎的工作就是要把指令转成JVM执行的语言(也可以理解成操作系统的语言),最后操作系统语言再转成计算机机器码。
4、解释器:一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
5、即时(Just-In-Time)编译器:即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
简单理解jit就是当代码中某些方法复用次数比较高的,并超过一个特定的值就成为了“热点代码”。那么这个这些热点代码就会被编译成本地代码(其实可以理解成缓存)加快访问速度。
6、本地native方法就是带有native标识符修饰的方法。native修饰符修饰的方法并不提供方法体,但因为其实现体是由非java代码在在外部实现的,因此不能与abstract连用;
存在的意义:不方便用java语言写的代码,使用更为专业的语言写更合适;甚至有些JVM的实现就是用c编写的,所以只能使用c来写。
更多的本地方法最好是与jdk的执行引擎的解释器语言一致(执行引擎、解释器);
Windows、Linux、UNIX、Dos操作系统的核心代码大部分是使用C和C++编写,底层接口用汇编编写.
为什么native方法修饰的修饰的方法PC程序计数器为undefined。读懂上面的所有知识点可以就很容易自己理解了。在一开始类加载时,native修饰的方法就被保存在了本地方法栈中,当需要调用native方法时,调用的是一个指向本地方法栈中某方法的地址,然后执行方法直接与操作系统交互,返回运行结果。整个过程并没有经过执行引擎的解释器把字节码解释成操作系统语言(也就是没有进入方法区?),PC计数器也就没有起作用。
举例:执行引擎运行时数据区各个模块协同工作
1、要执行的代码:
2、先执行main方法
3、调用其他方法
(三)GC垃圾回收机制
堆内存:类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
1、新生代(分为Eden,From Survivor,ToSurvivor)
(1)绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。
a、Eden是连续的内存空间,因此在其上分配内存极快
b、每次新生代的垃圾回收(Minor GC)采用copy算法,每次Eden区满的时候执行Minor GC
(2)当Eden区满的时候,执行Minor GC,将消亡的对象清理掉。最初一次将剩余的对象复制到一个存活区From(此时TO是空白的,两个Survivor总有一个是空白的)
(3)下次Eden区满的时候,执行Minor GC,将消亡的对象清理掉。将存活的对象复制到TO中。然后清空From区
(4)之后from和to一直交换角色,不管怎么样一定有一个是空的
(5)直到TO区被填满,TO区被填满后会将所有对象移动到老年代中。
PS:即便没有填满,两个存活区切换了几次(每进行一个MinorGC,会将所有存活的对象做一个标记+1,当标记值大于15的时候,用-XX:MaxTenuringThreshold控制移动到年老代中)
2、老年代(新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高)
(1)老年代的垃圾回收(Major GC)通常使用“标记-清理”或“标记-整理”算法
(2)当老年代空间不足时,会触发Major GC/Full GC,速度比一般GC慢10倍以上
PS:整堆包括新生代和老年代的垃圾回收称为Full GC。
3、持久代(永久代,也就是方法区)
(1)在JDK8之前的HotSpot实现中,类的元数据如方法数据、方法信息(字节码,栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过参数-XX:MaxPermSize进行设置。GC不会在主程序运行期对永久区域进行清理,这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
伴随着GC的就是OOM(OutOfMemory异常)
1、如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
2、如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:
a.程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
b.大量动态反射生成的类不断被加载,最终导致Perm区被占满。
说明:Jdk1.6及之前:常量池分配在永久代 ;Jdk1.7:有,但已经逐步“去永久代” 。Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
JVM结构图各模块生命周期总结
1、启动一个JVM虚拟机程序就是启动了一个进程,启动的同时就在操作系统的堆内存中开辟一块JVM内存区
2、虚拟机栈、本地方法栈、程序计数器这三个模块是线程私有的,有多少线程就有多少个这三个模块,生命周期跟所属线程的生命周期一致
PS:如程序计数器,因为多线程是通过线程轮流切换和分配执行时间来实现,所以当线程切回到正确执行位置,每个线程都由独立的程序计数器,各个线程之间的计数器互不影响,独立存储
3、其余是跟JVM虚拟机的生命周期一致
其它还有本地库接口和本地方法库
本地方法库接口:即操作系统所使用的编程语言的方法集,是归属于操作系统的
本地方法库保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的
在java代码中会通过System.loadLibrary("")加载C语言库(本地方法库)直接与操作系统平台交互
JDK,JRE,JVM的关系
1、JDK(Java Development Kit)是JAVA语言的软件开发工具包(SDK)。
2、在JDK的安装目录下有一个JRE目录,里面有两个文件夹bin和lib,其中bin里面的就是jvm,lib则是jvm工作所需要的类库