JVM介绍
- JDK,JRE,JVM:
(1)JRE(JAVA Runtime Environment): java 运行环境
java的平台,所有的java程序都在该平台下运行;
(2)JDK(JAVA Development kit): java开发工具
是程序开发者用来编译、调试java程序;jdk也是java程序,是需要在JRE上运行的,为了保证jdk的独立性,在jdk安装过程中,也需要安装jre,在jdk目录下有一个目录:jre,就是jre相关的包存放在该处;
(3)JVM(JAVA Vritual Mechine): java虚拟机
是jre的一部分,是一个虚拟出来的计算机,在该虚拟计算机上模拟出java程序的运行坏境:堆、栈。。。指令系统。
- JVM的生命周期:
- 虚拟机产生的起点:JAVA虚拟机实例通过调用某个初始化类的特定(main)方法,这个方法必须是共有的(public)、无返回值的(void)、静态的(static)并且可以接受一个字符串的数组作为参数(String[] args);
如果该方法命名不是(main)是否可以能实例化虚拟机???
(能否编译)不能,main是程序的入口。
- 虚拟机结束点:main方法执行结束(),System.exit()也可使虚拟机结束。
线程、内存空间部分区域(堆)也会进行划分。
* JVM工作过程:
将java程序加载到内存中,并最终执行。
(1)类加载:对字节码文件(.class文件)的加载;类加载子系统(Class Loader Subsystem);
(2)内存模型;(堆,栈,方法区,本地方法栈,程序计数器);内存空间(运行时数据区:Runtime data areas);
(3)程序执行需要执行引擎;执行引擎(Execution Engine);
类加载
- 类加载的时机:
虚拟机规范中明确了在6种情况下会对类进行一个加载;
(1)创建对象实例:new对象的时候,会对类进行初始化,前提是这个类没有被初始化;
(2)通过class文件反射创建对象;
(3)调用类的静态属性或静态属性赋值;
(4)调用类的静态方法;
(5)初始化一个类的子类,使用子类的时候先初始化父类;
(6)JAVA虚拟机启动时被标记为启动类的类:比如main方法所在的类;
- 类不会被加载的情况:
(1)在同一个虚拟机中一个类只能被加载一次,如果已经被初始化的类不被加载;
(2)在编译时能确定下来的静态变量,不会对类进行初始化;
- JVM中的类加载器的执行:
双亲委派模型
JAVA中提供三个默认的类的加载器:
(1)Bootstrap ClassLoader(启动类加载器) : 加载$JAVA_HOME中的jre/lib/rt.jar里所有的类;
(2)**Extension ClassLoader(标准扩展类加载器) :**负责加载JAVA平台上的扩展功能的jar包,包含¥JAVA_HOME中jre/lib/*.jar或者——DJava.ext.dirs指定目录下的jar包;
(3)Application ClassLoader(系统类加载器) : 负责加载classpath中指定到的jar和目录中的class;
(4)Custom ClassLoader:自定义类加载器;
类查找的顺序:向上委派;先向上委托,委托类找不到,当前类再查找;
类查找的过程:从Bootstrap开始查找-----》 Extension -----》Application—》Custom
从上到下都找不到则抛出ClassNotFoundException异常;
- 类的加载描述:-------》双亲委派模型(如果一个类加载器的请求,不会自己尝试加载这个类,而是将请求委派给父类加载器去完成,依次向上)
1.当applicationClassLoader加载一个class时,不会自己去尝试加载这个类,而是将类加载请求委托给父类加载器extensionClassLoader去完成;
2.当extensionClassLoader加载class时,不会自己去尝试加载这个类,而是将类加载请求委托给父类加载器BootstrapClassLoader去完成;
3.如果Bootstrap ClassLoader加载失败,会使用ExtensionClassLoader尝试加载;
4.如果Extension ClassLoader加载失败,会使用Application ClassLoader尝试加载;
5.如果Application ClassLoader也加载失败,则会抛出ClassNotFoundException;
注:以上只要一个加载器加载成功的话,就会直接返回该类;
以上就是双亲委派模型:如果一个类加载器收到类加载的请求,他不会自己尝试
加载这个类,而是将请求委派给父加载器去往上加载,依次向上
好处:
(1)防止内存中出现多份同样的字节码(不同类加载器指定的目录下才起作用);
(2)单一性机制:因为委派机制的关系,一个类(唯一的全限定名)只能被一个类加载器一次;
- 类加载过程:
加载:查找并加载类的class文件(双亲委派机制);
验证:文件的格式、原数据、字节码、符号引用;
准备:为类中的静态变量分配空间,并将其初始化为默认值;
解析:将类中的符号引用转化为直接引用;
初始化:为类中的静态变量赋予正确的初始值;
执行引擎:类加载器将字节码载入内存后,执行引擎以java字节码为单元,读取java字节码。java字节码机器读不懂,必须将字节码转化为平台相关的机器码。这个过程就是由执行引擎完成的。
内存模型
- 划分jvm空间:
(1)线程共享:
堆:创建的对象存放的区域,也是垃圾回收的主要区域;
方法区:类信息、静态变量,常量;
(2)线程私有:
程序计数器:标识,当前程序执行的字节码位置指示器;
虚拟机栈:保存局部变量,基本的数据类型,以及对象的地址引用;
本地方法栈:为JVM提供服务的Native方法(C)提供服务;
程序计数器:
JVM内存空间占用较小的空间,
指示当前程序执行的行号指示器,
程序计数器是线程私有的,
程序计数器的生命周期和线程的生命周期是一样的,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
程序计数器是内存模型中唯一不会抛出异常(OutOfMemoryError)的区域。
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,
分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,
各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
主要作用有两个:
(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,
如:顺序执行、选择、循环、异常处理。
(2)在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
java虚拟机栈:
描述的是 Java 方法执行的内存模型。
线程私有,生命周期和线程是同步的,每个线程都有各自的Java虚拟机栈,
而且随着线程的创建而创建,随着线程的死亡而死亡。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),
其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。
(实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表
主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、
对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种异常:
StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError: 若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
本地方法栈:
和虚拟机栈所发挥的作用非常相似,
区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,
用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,
也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
堆:
Java 虚拟机所管理的内存中最大的一块,
Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,
所以Java堆还可以细分为:新生代和老年代:
在细致一点有:Eden空间、From Survivor、To Survivor空间等。
进一步划分的目的是更好地回收内存,或者更快地分配内存。
永久带说明:
Jdk1.6及之前:常量池分配在永久代 。
Jdk1.7:有,但已经逐步“去永久代” 。
Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域
(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
方法区:
方法区与 Java 堆一样,是各个线程共享的内存区域,
它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。
仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,
这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。
但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,
还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,
当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。