2、JVM的基本介绍
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
jvm是直接与操作系统进行交互的,与操作系统交互的结构图如下,
jvm是直接与操作系统进行交互,不会直接与服务器硬件进行交互,可以简单理解jvm就是一台小的电脑,一台运行在我们操作系统之上的虚拟的电脑
3、一个java文件的生命之旅
1、java文件经过编译之后变成class字节码文件
2、字节码文件通过类加载器被搬运到jvm虚拟机当中来
3、虚拟机当中主要由五大块
a)方法区:线程共享区域,存在多线程安全问题,主要存放全局变量,常量,静态变量等
b)堆:线程共享区域,存在多线程安全问题,主要存放对象实例和数组等
c)虚拟机栈:线程不同享区域,不存在多线程安全问题,主要存放局部变量等
d)本地方法栈:线程不共享区域,不存在多线程安全问题,主要负责去调用一些底层的C程序实现,一般不做研究
e)程序计数器:线程不共享区域,不存在多线程安全问题,一般不做研究
4、类加载器的基本介绍
类加载器负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Enigine决定
1、类加载器的过程
从类被加载到虚拟机内存中开始,到卸载内存位置,它的整个生命周期分为7个阶段:
加载(loading)、验证(verification)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(Using)、卸载(Unloading)。其中验证、准备、解析三个部分统称为连接。7个阶段发生的顺序如下:
1、加载:
1、将class文件加载到内存中。
2、将静态数据结构(数据存在于class文件的结构)转换成方法区中运行时的数据结构。
注意:方法区中如果出现OOM,那么多半是因为加载的依赖太多
4、在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口
2、链接
1、验证:确保加载的类符合JVM规范与安全。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
2、准备:为static变量在方法区中分配空间,设置变量的初始化。例如static int a=3,在此阶段会a被初始化为0;
注意:准备阶段,只设置类中的静态变量(方法区中),不包括实例变量(堆内存中)实例变量是在初始化的时候分配值的
3、解析:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:简单的理解就是字符串,比如引入一个类,java.util.ArrayList这就是一个符号引用
直接引用:指针或者地址偏移量。引用对象一定在内存(已经加载)。
3、初始化
初始化是类加载的最后阶段,初始化阶段是执行类构造器<clinit>()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值运动()和静态变量和静态语句块static{}合并
初始化,为类的静态变量赋予正确的初始值
4、使用,卸载
使用:正常使用
卸载:GC把无用的对象从内存中卸载
2、类加载器的加载顺序
加载一个类进来之后,需要经过加载,链接,初始化,使用,卸载等一系列步骤,那么我们加载一个class类的顺序也是有优先级的,先加载我们rt.jar这个jar包下面的class类,所以我们才能够使用如下代码
1)bootstrap ClassLoader
负责加载JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。
3)APP ClassLoader
负责加载classpath中指定的jar包及目录中class。
4)Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如Tomcat、jboss都会根据J2EE规范自行实现ClassLoader。加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只有某个Classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
在获取ExtClassLoader的父loader的时候出现了null,这里因为Bootstrap Loader(引导类加载器)是用C++语言实现的,找到一个确定的返回父Loader的方式,于是就返回null
3、类加载器当中的双亲委派机制
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父亲加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是那个加载器加载这类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载最终得到了都是同样一个Object对象。
运行结果报错如下:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
为了保证我们开发的代码,不会污染java当中自带的源代码,保证我们使用的class类都是最终的一个,所以才会有双亲委派这种机制,提供一种沙箱环境来保证我们加载的class类都是同一个
遵循一个原则:层层向上找,先找到先使用,后面的一概不见
5、运行时的数据区域
1、本地方法栈Native Method Stack
本地方法栈(Native Method Stacks)与虚拟机所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到native方法服务(比如C语言的程序和C++写的程序)
2、本地接口Native Interface
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。
3、程序计数器/PC寄存器
PC寄存器就是一个指针,指向我们下一个需要运行的方法,程序计数器是一块非常小的内存空间,主要是用来对当前线程所执行的字节码的行号指示器;
而且程序计数器是内存区域中唯一一块不存在OutOfMemoryError的区域
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果执行的是一个Native方法,那这个计数器是空的。
用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误
4、方法区Method Area
方法区和java堆一样,是线程共享的区域;它存储每一个类的结构信息
方法区的作用的就是用来存储:已经被虚拟机加载的类信息,常量,静态常量等;
而且方法区还有另一种叫法:【非堆】,也有人给方法区叫做永久代
当方法区存储信息过大时候,也就是无法满足内存分配的时候保存
上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。
But
实例变量存在堆内存中,和方法区无关
5、java虚拟机栈
栈管运行,堆管存储
1、虚拟机栈的基本介绍
程序员经常说“堆栈”,其中的栈就是虚拟机栈,更确切的说,大家谈的栈是虚拟机中的局部变量表部分;
虚拟机栈藐视的是:Java方法执行的内存模型;(说白了就是:虚拟机栈就是用来存储:局部变量、栈操作
动态链表、方法出口这些东西;
这些东西有个特点:都是线程私有的,所以虚拟机栈是线程私有的)
对于虚拟机栈可能出现的异常有两种:
1:如果线程请求的栈深度大于虚拟机栈允许的最大深度,报错:StackOverflowError
(这种错误经常出现在递归操作中,无限制的反复调用方法,最终导致压栈深度超过虚拟机允许的最大深度,就会报错)
2:java的虚拟机栈可以进行动态扩展,但随着扩展会不断的申请内存,当无法申请足够内存的时候就会报错:OutOfMemoryError
2、虚拟机栈的生命周期
对于栈来说,不存在垃圾回收问题,只要程序执行结束,栈就over释放,生命周期和线程一致,是私有线程区域。8钟基本类型的变量+对象的引用变量+实例方法都是函数栈内存中分配。
3、虚拟机栈当中究竟存放了什么数据
栈帧中主要保存3类数据:
局部变量(Local Variables):输入参数和输出参数以及方法内的变量。八种基本数据类型+引用类型(string,以及自己定义的class类等)
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等。
4、虚拟机栈运行原理
栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,约等于1Mb左右。
5、局部变量复用slot
局部变量表用于存放方法参数和方法内部定义的局部变量。方法表的Code属性: max_locals 数据项指明了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以Slot(Variable Slot:变量槽)为最小单位,其中64位长度的long和double类型的数据占用2个Slot,其余数据类型(boolean、byte、char、short、int、float、reference、returnAddress)占用一个Slot(一个Slot可以存放32位以内的数据类型)
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0到局部变量表最大Slot数量。32位数据类型的变量,索引n就代表使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程,如果是实例方法(非static的方法)那么局部变量表中第0位的Slot默认是用于传递方法所属实例对象的引用,在方法中可以通过 this 关键字来访问到这个隐含的参数,其余参数则按参数表顺序排列,占用从索引1开始的局部变量Slot。参数列表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
为了节省栈帧空间,局部变量表中的Slot是可以复用的,当方法执行位置已经(程序计数器在字节码的值)超过了某个变量,
那么这个变量的Slot可以被其他变量复用。
除了能节省栈帧空间,还伴随着可能会影响到系统垃圾收集的行为。
在IDEA当中执行以上代码,并加上参数 -verbose:gc 来查看我们的GC回收信息,我们会发现第一个方法没有GC回收,第二个方法没有垃圾回收,第三个方法GC回收了
能否被回收的关键因素就是变量是否还有被引用,在第三个示例当中int a = 0;这个变量没有被其他引用,导致垃圾回收工作,回收垃圾
6、方法的参数对调用次数的影响
如果一个方法有参数列表,一个方法没有参数列表,形式参见如下
运行不同的重载方法,我们会明显的发现,没有参数的方法,调用的次数会更多,因为我们的参数就是局部变量,局部变量的装载也需要一定的内存空间
我们可以通过参数 -Xss160K 来调整我们的每个线程的堆栈大小,但是无论如何调整栈空间大小,依然会发现没有参数且没有局部变量的方法调用次数远远比有参数有局部变量的方法调用的次数多得多。栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右
6、java虚拟机堆
1、JVM虚拟机堆的基本组成介绍
1.JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Feneration)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
2.年轻代又分为Eden和Survivor区。Survivor区中由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1.
3.堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
4.非堆内存用途;永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代类型,都是方法区的实现,它们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有两个参数:
MetaspaceSize:初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize:限制元空间大小上限,防止异常占用过多物理内存
2、jdk1.8为什么要移除永久代
永久代的功能类似,都是用于存放一些不会被回收的对象,例如我们的数据库连接池这样的对象等,只不过在1.8当中元数据区最大的特性就是使用了堆外内存,也就是操作系统级别的物理内存,数据直接保存到了物理内存当中去了。
移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不会再出现永久代OOM问题了!
3、新生代介绍
新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,进行养老区的内存清理,所以对响应要求高的应用尽量减少发生Major GC(Full GC),避免响应超时,。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代
所有GC都会停止应用所有线程。