当咱们第一次学习Java时这些原理上的东西就会被提到,可是不多有真正去学习。今天开始从头过一遍Java,打算从JVM开始。
html
JVM是Java Virtual Machine的缩写。它是一种基于计算设备的规范,是一台虚拟机,即虚构的计算机。vue
JVM屏蔽了具体操做系统平台的信息(显然,就像是咱们在电脑上开了个虚拟机同样),固然,JVM执行字节码时实际上仍是要解释成具体操做平台的机器指令的。java
经过JVM,Java实现了平台无关性,Java语言在不一样平台运行时不须要从新编译,只须要在该平台上部署JVM就能够了。于是能实现一次编译多处运行。(就像是你的虚拟机也能够在任何安了VMWare的系统上运行)数组
JRE:Java Runtime Environment,也就是JVM的运行平台,联系平时用的虚拟机,大概能够理解成JRE=虚拟机平台+虚拟机本体(JVM)。相似于你电脑上的VMWare+适用于VMWare的Ubuntu虚拟机。这样咱们也就明白了JVM究竟是个什么。缓存
JDK:Java Develop Kit,Java的开发工具包,JDK本体也是Java程序,所以运行依赖于JRE,因为须要保持JDK的独立性与完整性,JDK的安装目录下一般也附有JRE。目前Oracle提供的Windows下的JDK安装工具会同时安装一个正常的JRE和隶属于JDK目录下的JRE。markdown
JVM主要包括:程序计数器(Program Counter),Java堆(Heap),Java虚拟机栈(Stack),本地方法栈(Native Stack),方法区(Method Area)多线程
详细的结构以下:并发
如今我来分别介绍一下每一部分的功能。app
是一个寄存器,能够看做是代码行号指示器,相似于实际计算机里的PC,用于指示,跳转下一条须要执行的命令。Java的基础操做以及异常处理等都十分依赖PC。jvm
JVM多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的。在一个肯定的时刻,一个处理器(或者说多核处理器的一个内核)只会执行一条线程中的命令。所以,为了正常的切换线程,每一个线程都会有一个独立的PC,各线程的PC不会互相影响。这个私有的PC所占的这块内存便是线程的“私有内存”。
若是线程在执行的是Java方法,那么PC记录的是正在执行的虚拟机字节码指令的地址。若是正在执行的不是Java方法即Native方法,那么PC的值为undefined。
PC的内存区域是惟一的没有规定任何OutOfMemoryError的Java虚拟机规范中的区域。
同PC同样(从工做流程图里咱们能够看到,实际上,PC也是存在于JVM Stack上的),也是线程私有的,生命周期与线程相同。虚拟机栈描述Java方法执行的内存模型,每一个方法被执行时都会建立一个栈帧(Stack Frame),栈帧会利用局部变量数组存储局部变量(Local Variables),操做栈(Operand Stack),方法出口(Return Value),动态链接(Current Class Constant Pool Reference)等信息。
局部变量数组存储了编译可知的八个基本类型(int, boolean, char, short, byte, long, float, double),对象引用(根据不一样的虚拟机实现多是引用地址的指针或者一个handle),returnAddress类型。64位的long和double会占用两个Slot,其他类型会占用一个Slot。在编译期间,局部变量所需的空间就会完成分配,动态运行期间不会改变所需的空间。
操做栈在执行字节码指令时会被用到,这种方式相似于原生的CPU寄存器,大部分JVM把时间花费在操做栈的花费上,操做栈和局部变量数组会频繁的交换数据。
动态链接控制着运行时常量池和栈帧的链接。全部方法和类的引用都会被看成符号的引用存在常量池中。符号引用是实际上并不指向物理内存地址的逻辑引用。JVM 能够选择符号引用解析的时机,一种是当类文件加载并校验经过后,这种解析方式被称为饥饿方式。另一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。不管如何 ,JVM 必需要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被彻底替换。若是一个类的符号引用尚未被解析,那么就会载入这个类。每一个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。
对Java虚拟机栈这个区域,Java虚拟机规范规定了两种异常:
本地方法栈如其名字,和Java Virtual Machine Stack其实极为相似,只是执行的是Native方法,为Native方法服务。在JVM规范中,没有对它的实现作具体规定。
Java堆是被全部线程共享的一块区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存(随着技术的发展,已不绝对)。
Java堆是垃圾收集器管理的主要区域,于是也被称为GC堆。收集器采用分代回收法,GC堆能够分为新生代(Yong Generation)和老生代(Old Generation)。新生代包括Eden Space和Survivor Space。但不管哪一个区域,如何划分,存储的都是Java对象实例,进一步的划分是为了更好的回收内存或快速的分配内存。
根据Java虚拟机规范,堆所在的物理内存区间能够是不连续的,只要逻辑连续就能够。实现时既能够是固定大小,也能够是可扩展的。若是堆没法扩展时,就会抛出OutOfMemoryError。
方法区和Java堆相似,也属于各线程共享的内存区域。用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码数据等。它属于非堆区(Non Heap),和Java堆区分开。对于存在永久代(Permanent)概念的虚拟机(HotSpot)而言,方法区存在于永久代。Java虚拟机规范对方法区的规定很宽松,甚至能够不实现GC。不过并不是进入方法区的数据就会永久存在了,这块区域的内存回收主要为常量池的回收和类型的卸载。这个区域的回收处理不善也会致使严重的内存泄漏。
当方法区没法知足内存分配需求时也会抛出OutOfMemoryError。
用于编译和存储那些被 JIT 编译器编译成原生代码的方法。
类信息存储在方法区,其主要构成为运行时常量池(Run-Time Constant Pool)和方法(Method Code)。
一个编译后的类文件包括如下结构:
结构 | 解释 |
---|---|
magic, minor_version, major_version | 类文件的版本信息和用于编译这个类的 JDK 版本。 |
constant_pool | 相似于符号表,尽管它包含更多数据。下面有更多的详细描述。 |
access_flags | 提供这个类的描述符列表。 |
this_class | 提供这个类全名的常量池(constant_pool)索引,好比org/jamesdbloom/foo/Bar。 |
super_class | 提供这个类的父类符号引用的常量池索引。 |
interfaces | 指向常量池的索引数组,提供那些被实现的接口的符号引用。 |
fields | 提供每一个字段完整描述的常量池索引数组。 |
methods | 指向constant_pool的索引数组,用于表示每一个方法签名的完整描述。若是这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码。 |
attributes | 不一样值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。 |
运行时常量池是方法区的一部分。Class文件中有类的版本,字段,方法,接口等描述信息和用于存放编译期生成的各类字面量和符号引用。这部份内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机规范对Class的细节有着严苛的要求而对运行时常量池的实现不作要求。通常来讲除了翻译的Class,翻译出来的直接引用也会存在运行时常量池中。
运行时常量池具有动态性,即运行时也可将新的常量放入池中。好比String类的intern()方法。
常量池没法申请到足够的内存分配时也会抛出OutOfMemoryError。
直接内存并不在Java虚拟机规范中,不是Java的一部分,可是也被频繁使用并可能致使OutOfMemoryError。Native函数库能够直接分配堆外内存,经过存储在Java堆里的DirectDataBuffer对象做为这块内存的引用进行操做。这样作在一些场景中能够显著提升性能。
直接内存是堆外内存,天然不受Java堆大小的限制,可是可能受实体机内存大小的限制。若是内存各部分总和大于实体机的内存时,也会报出OutOfMemoryError。
将内存中再也不被使用的对象进行回收,GC中用于回收的方法称为收集器,因为GC须要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽量的缩短GC对应用形成的暂停。
不一样的对象引用类型, GC会采用不一样的方法进行回收,JVM对象的引用分为了四种类型:
JVM容许一个程序使用多个并发线程,Hotspot JVM中Java的线程与原生操做系统的线程是直接映射关系。即当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好之后,就会建立一个操做系统原生线程。Java 线程结束,原生线程随之被回收。操做系统负责调度全部线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认因为它的结束是否要终止 JVM 进程(好比这个线程是最后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的全部资源。