JVM知识查漏补缺学习记录
JVM知识查漏补缺学习记录
-
JVM介绍
-
jvm是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
-
jvm包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。
-
JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
-
-
JDK、JRE、JVM三者关系
-
JRE(Java Runtime Environment),也就是java平台。所有的java程序都要在JRE环境下才能运行。
-
JDK(Java Development Kit),是开发者用来编译、调试程序用的开发包。JDK也是JAVA程序需要在JRE上运行。
-
JVM(Java Virtual Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。
-
-
JVM原理及执行程序的过程
JVM是Java核心和基础,在Java编译器和os平台之间的虚拟处理器。它可以在上面执行Java的字节码程序。Java编译器只要面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译成字节码程序,通过JVM将每一条指令翻译成不同平台的机器码,通过特定平台运行。
JVM执行程序的过程如下图所示:
-
.java文件
即使用Java语言编写的程序
-
.class文件
* Java程序经过Java编译器进行编译,生成.class文件,java编译一个类时,如果这个类所依赖的类还没有被编译,
* 编译器会自动编译所依赖的类,然后引用。如果java编译器在指定的目录下找不到该类所依赖的类的 .class文件或者 .java源文件,就会报
Can't found symbol
的异常错误。* 编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的(常量、类名、成员变量等)以及符号引用(类引用、方法引用,成员变量引用等);方法字节码中放的是各个方法的字节码。
-
类加载器
-
字节码校验器
-
解释器
-
JIT代码生成器
-
-
JVM类的生命周期
加载-》连接-》初始化-》使用-》卸载
-
JVM内存模型
(1)分类
-
堆内存(Heap)
* 堆内存是Java虚拟机所管理的内存中最大的一块区域。
* Java堆被所有线程所共享。
* 在虚拟机启动时创建,主要用来存放对象实例。
* Java堆只要求逻辑上连续即可。
* 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
此外,Java堆也是垃圾收集器管理的主要区域,所以也叫“GC”堆。由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:
- 年轻代(Young Generation)
- 伊甸区(Eden)
- 幸存者区域(Survivor Sapce。包括s0,s1)
- 老年代(Old/Tenured Generation)
- 年轻代(Young Generation)
-
方法区(Method Area 或者 Non-Heap非堆 或者 PermGen持久代)
* 方法区存储类信息、常量、静态变量等数据,是线程共享的区域。
-
栈(Thread)
* 主要用于方法的执行。
* java栈、本地方法栈、程序计数器是运行时线程私有的内存区域。
-
Java虚拟机栈
* Java虚拟机栈的生命周期与线程相同。
* 虚拟机栈描述的是Java方法执行的内存模型。
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、引用类型和returnAddress类型(指向了一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
JVM对虚拟机栈区域两种异常状况的规定:
1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2)如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
-
本地方法栈
* 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
* 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
-
程序计数器
* 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
* 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)
* 程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
-
(2)控制参数
参数 含义 -Xms 设置堆的最小空间大小。 -Xmx 设置堆的最大空间大小。 -XX:NewSize 设置新生代最小空间大小。 -XX:MaxNewSize 设置新生代最大空间大小。 -XX:PermSize 设置非堆区最小空间大小。 mSize 设置非堆区最大空间大小。 -Xss 设置每个线程的堆栈大小。 注:没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。有以下公式:
老年代空间大小 = 堆空间大小 - 年轻代空间大小
-
-
JVM内存分配过程
1)JVM 会试图为相关Java对象在Eden中初始化一块内存区域。
2)当Eden空间足够时,内存申请结束;否则到下一步。
3)JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收)。释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区。
4)Survivor区被用来作为Eden及Old的中间交换区域,当Old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区。
5)当Old区空间不够时,JVM 会在Old区进行完全的垃圾收集(0级)。
6)完全垃圾收集后,若Survivor及Old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”错误。 -
类加载器
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。
一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
-
根加载器(bootstrap class loader)
它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。
-
扩展类加载器 (extensions class loader)
它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
-
系统类加载器(system class loader)
被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
-
类加载器加载类的一般步骤
- 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
- 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
- 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
- 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
- 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
- 从文件中载入Class,成功后跳至第8步。
- 抛出ClassNotFountException异常。
- 返回对应的java.lang.Class对象。
-
-
类加载过程
-
加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:
- 从本地文件系统加载class文件,这是大部分程序的类加载方式。
- 从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
- 通过网络加载class文件。
- 把一个Java源文件动态编译,并执行加载。
-
连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段:
-
验证
验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
-
文件格式验证
主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。
例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
-
元数据验证
对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
-
字节码验证
最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
-
符号引用验证
主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
-
-
准备
类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
-
解析
将类的二进制数据中的符号引用替换成直接引用。
符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
-
-
初始化
初始化是为类的静态变量赋予正确的初始值,
-
准备阶段和初始化阶段的区别
例,如果类中有语句:
private static int a = 10
,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
-
-
使用
-
卸载
-
-
类加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“com.lyj.load”))
- 加载一个类的子类(会首先加载子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
-
类加载机制
-
全盘负责
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
-
双亲委派
所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
-
缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
-
-
双亲委派模型
-
工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
-
优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
-