jvm基础
一、基础概念
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
JVM 核心内部组件如下图所示
栈(stack):
通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;即声明的变量属于什么类型时,该变量都保存在栈(stack)空间里。
堆(heap)
而通过new关键字和构造器创建的对象放在堆空间;
如 Person person=new Person();中person(对象的引用)存储在stack中,而new Person()出来的实例则存储在heap中。
总结:
栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间中
jvm运行流程:
总的运行流程如下:
详细流程如下:
所有的Java程序代码必须保存在.java的文件之中,这些称为源代码。而这些源代码并不能够直接执行,必须使用javac.exe命令将其编译为.class文件,而后利用java.exe命令在JVM进程之中解释此程序。即 首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
JVM内存
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
1.程序计数器
想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。
虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。
在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
简单的说就是用来指示 执行哪条指令的。
2.Java栈
Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。
3.本地方法栈
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以*实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
4.堆(heap)
用来存储类的实例,如通过new关键字和构造器创建的对象放在堆空间;
如 Person person=new Person();中person(对象的引用)存储在stack中,而new Person()出来的实例则存储在heap中
在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。那么在Java中是怎么样的呢?
Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。
5.方法区
方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
java堆内存模型
JDK1.7以前
JDK1.8
JDK 1.8之后将最初的永久代内存空间取消了为了将HotSpot与JRockit两个虚拟机标准联合为一个.
新生代:新的对象和没有达到一定“年龄”的对象对象存放空间(活跃对象)
老年代:被长时间使用的对象,老年代空间要相对较大
元空间:一些操作的临时对象,如方法中的临时对象,直接使用物理内存
注意:永久代和元空间都是存放临时对象的,但是永久代使用的是JVM直接分配的内存,而元空间使用的是物理内存
Java垃圾回收机制
什么是垃圾回收?
顾名思义,垃圾回收就是释放垃圾占用的空间,那么在Java中,什么样的对象会被认定为“垃圾”?那么当一些对象被确定为垃圾之后,采用什么样的策略来进行回收(释放空间)?在目前的商业虚拟机中,有哪些典型的垃圾收集器?下面我们就来逐一探讨这些问题。以下是本文的目录大纲:
一.如何确定某个对象是“垃圾”?
二.典型的垃圾收集算法
三.典型的垃圾收集器
1.如何确定某个对象是“垃圾”?
主要有以下几种
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象
Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;
原来的obj所引用的对象已经不能使用了,故可以被标记为垃圾对象。obj1 已经指向了另一个对象obj2,故obj1原来所指向的对象也将被标记为垃圾对象
2)局部引用所指向的对象,比如下面这段代码:
void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}
循环每执行完一次,生成的Object对象都会成为可回收的对象。
3)只有弱引用与其关联的对象,比如:(暂时无法理解)
WeakReference<String> wr = new WeakReference<String>(new String("world"));
2.典型的垃圾收集算法
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间
缺陷:容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2.Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
缺陷:
对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。而且Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
3.Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
分代收集的主要流程如下:
注意:在整个GC的流程之中是 针对新生代和老年代进行内存清理的操作,而元空间和永久代都不在GC的范围之内。
1. 当新创建一个对象的时候,那么对象一定需要在堆内存中分配内存空间。所以就需要对该对象申请内存空间
2. 首先会判断Eden区中是否有充足的内存空间,如果有,那么直接将对象保存在Eden区中
3. 如果此时Eden中内存空间不足,那么会自动执行一个Minor GC的操作,将伊甸园区的无用的内存空间进行清理,清理之后
继续判断伊甸园区的内存空间是否充足。如果充足,那么将对象直接在疑点园区中进行内存分配。
4. 如果执行了MinorGC之后发现伊甸园区的内存依然不足,则会对存活区进行判断,如果存活区内存足够。那么将伊甸园区的
一部分活跃对象保存到存活区,随后继续判断伊甸园区内存,如果够进行新对象的内存分配。
5. 如果此时存活区没有足够的内存空间,则继续判断老年区。如果老年区的内存空间充足,则将存活的部分活跃对象保存到
老年代,而后存活区会出现剩余空间。随后将伊甸园区的活跃对象保存对象,
然后在伊甸园区开辟空间,保存新对象。
6. 如果此时老年代也没有剩余空间,则执行MajorGC(FullGC),清理老年代内存
7. 如果执行了FullGC之后依然无法保存对象,就会产生OOM异常“OutofMemoryError”。
3.典型的垃圾收集器
最主要的收集器为G1收集器
@see Java垃圾回收机制https://www.cnblogs.com/dolphin0520/p/3783345.html
参考资料:
1.浅析JVM 第一篇(JVM执行流程)http://blog.csdn.net/qq_25235807/article/details/61920877
2.JVM的内存区域划分https://www.cnblogs.com/dolphin0520/p/3613043.html
3.Java垃圾回收机制https://www.cnblogs.com/dolphin0520/p/3783345.html
4.浅析JVM 第二篇(JVM内存模型和垃圾回收)https://blog.csdn.net/qq_25235807/article/details/61929343