一、JDK、JRE、JVM
-
JDK(Jave Devolemnet Kit):Java开发工具
- 是程序开发者用来编译、调试JAVA程序的工具包
- JDK也是Java程序,需要在JRE上运行
- 为了保证JDK的独立性和完整性,在JDK安装过程中,也需要安装JRE,在jdk目录下有一个目录:jre,就是jre相关的包存放在该处
-
JRE(Java Runtime Environment):Java运行环境
- 也叫做Java运行平台,所有的JAVA程序要在jre上才能运行。
-
JVM(Java Virtual Machinel):Java 虚拟机
- 是JRE的一部分,是一个虚拟出来的计算机,通过在真实的机器上仿真模拟各种计算机功能的实现。
- JVM有自己的一套硬件架构,包括处理器、堆栈、寄存器等,还有相应的指令系统。
- 最大的特点:支持跨平台性,实现与操作系统无关,实现跨平台性
图源网络,侵删
二、JVM的生命周期
JVM负责运行Java程序,当启动Java程序时,一个JVM实例产生,当程序关闭退出时,JVM也就随之消亡了。
- Java虚拟机产生的起点:
- JAVA虚拟机实例通过调用某个初始化类的特点(main)方法
- 这个方法必须是共有的(public)、无返回值的(void)、静态的(static)
- 并且可以接受一个字符串的数组作为参数(String[] args)
- 任何一个拥有这个main方法的类都可以作为Java程序的起点
- JAVA虚拟机实例通过调用某个初始化类的特点(main)方法
- Java虚拟机结束点:
- mian方法执行结束
- System.exit()也是可以使虚拟机结束
- Java的两种线程:
- 用户线程(main)
- 守护线程(垃圾回收线程)
三、JVM的工作过程
图源见水印
JVM划分了三个子系统:
- 类加载子系统(Class Loader Subsystem)
- 运行时数据区(Runtime Data Areas)
- 执行引擎(Execution Engine)
1、类加载子系统
Java的动态类加载功能由类加载子系统来实现。它可以装载、链接、初始化类文件(第一次引用时需要初始化)。
- 装载:
其功能是加载类。共有三中类装载器:Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader - 链接
- 验证:字节码验证器将验证生成的字节码是否正确,如果验证失败,将得到验证错误的信息
- 准备:对于所有静态变量,内存将被分配并分配默认值
- 解析:所有符号内存引用都替换为来自方法区域的原始引用
- 初始化:
静态变量都将被赋予原始值,静态代码块将被执行
2、运行时数据区域
- 方法区: 类级别数据、静态变量存储位置
- 堆: 对象及其对实例变量和数组的存储位置,也是垃圾回收的主要区域
- 虚拟机栈: 保存局部变量,基本的数据类型,以及对象的地址引用
- 生命周期和线程是同步的
- 异常:
- OutOfMemoryError:允许栈空间动态扩容,空间在不足的情况下抛出的异常
- StatckoverflowError:不允许栈空间动态扩容,在空间不足时会抛出的异常
- 本地方法栈: 保存本地方法的信息,线程私有
- 程序计数器:
- JVM内存空间占用较小的空间,指示当前程序执行的行号指示器
- 程序计数器的生命周期和线程的生命周期是一样的
- 程序计数器是内存模型中唯一不会抛出异常(OutOfMemoryError)的区域
3、执行引擎
分配给运行时数据区域的字节码由执行引擎执行,执行引擎读取字节码并逐个执行它。
四、内存模型
1.程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
主要作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2.虚拟机栈
与程序计数器一样,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 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
3.本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是:
- 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务
- 而本地方法栈则为虚拟机使用到的 Native 方法服务
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
4.堆
堆是Java 虚拟机所管理的内存中最大的一块, Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。
从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代。
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
- Eden区:
Eden区是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。 - Survival区:
Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survivalfrom区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。 - 永久带说明:
- Jdk1.6及之前:常量池分配在永久代 。
- Jdk1.7:有,但已经逐步“去永久代” 。
- Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
5.方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
运行时常量池:
运行时常量池是方法区的一部分。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。