总计JVM的一个体系架构
为了更好的理解整个架构、更好的理解代码以及面试的必备知识点梳理
整个框架的体系结构如下:
(灰色区域为线程私有,不存在垃圾回收;深色区域为共享区域,存在垃圾回收)
源代码文件(.java后缀)被Java编译器编译为字节码文件(.class后缀)
由JVM的类加载器加载各个类的字节码文件,交由JVM执行引擎执行
在JVM执行过程中,会存储程序执行期间需要用到的数据和相关信息(运行时数据区)
其运行时的时区包括如下内容:
这部分内容有个重要的概念:
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。
加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
具体双亲委派的机制是:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
类似PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的TVM指令地址,如果是在执行native方法,则是未指定值(undefned) 。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
栈是运行时的单位,而堆是存储的单位
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
Java虚拟机栈(Java virtual Machine stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack Frame),对应着一次次的Java方法调用。
这是是线程私有的,其生命周期和线程一致
主要的作用是管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
栈的特点(优点)
栈的存储时:
·每个线释都有自己的栈,栈中的数据都是以栈帧(stack Frame)的格式存在
在这个线程上正在执行的每个方法都各自对应一个栈帧(stack Frame)。
·栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
每个栈帧中存储着:
局部变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型
包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
采用slot(卡槽)的索引访问
操作数栈
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-out)的操作数栈,也可以称之为表达式栈(Expression stack) 。
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop) .
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
比如:执行复制、交换、求和等操作
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
主要通过出栈和入栈的访问
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
常量池主要是提供符号和常量便于指令的识别
本地方法栈与Java栈的作用和原理非常相似。
区别是Java栈是为执行Java方法的调用,而本地方法栈则是为管理本地方法(Native Method)调用的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的。
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。所以堆,是Gc ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。
Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)
其中年轻代又可以划分为Eden空间、Survivor0空间和survivor1空间(有时也叫做from区、to区) 。
几乎所有的Java对象都是在Eden区被new出来的。
绝大部分的Java对象的销毁都在新生代进行了。
对象分配的一个过程:
:-XX:MaxTenuringThreshold=
进行设置。补充oom异常
堆内存满了,产生错误
由于99%的对象都是临时对象,所以oom错误发生的机率非常低
当堆内存满了会进行垃圾回收等操作
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC) ,一种是整堆收集((Full GC), 部分收集:不是完整收集整个Java堆的垃圾收集。
其中又分为
新生代收集(Minor GC / Young GC) :只是新生代的垃圾收集
老年代收集(Major GC / old GC) :只是老年代的垃圾收集。
目前,只有cMS Gc会有单独收集老年代的行为。
注意,很多时候Major GC会和Full Gc混淆使用,需要具体分辨是老年代
回收还是整堆回收。
混合收集(Mixed Gc):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 GC会有这种行为
整堆收集(Fu11 GC):收集整个java堆和方法区的垃圾收集。
年轻代Gc(Minor GC)触发机制:
老年代Gc (Major GC/Full GC)触发机制:
之所以堆要分区:唯一理由就是优化cc性能。
如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
具体内存的分配策略是:
如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被survivor容纳的话,将被移动到survivor 空间中,并将对象年龄设为1 。对象在survivor区中每熬过一次MinorGc ,年龄就增加1 岁,当它的年龄增加到一定程度(默认为15 岁,其实每个JVM、每个cc都有所不同)时,就会被晋升到老年代中。
它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
关闭JVM就会释放这个区域的内存。
在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
永久代、元空间二者并不只是名字变了,内部结构也调整了。
如何解决OOM的问题: