一、什么是JVM虚拟机
JVM是Java Virtual Machine(Java虚拟机)的缩写,是一种用于计算机设备的规范,它是一个虚构出来的计算机,通过在实际计算机上仿真模拟各种计算机功能来实现的。只要计算机设备上安装了JVM,就可以执行编译后的字节码文件(.class),并可跨平台使用。
JVM的五大模块:
类装载器:负责加载class文件
运行时数据区:
执行引擎:负责解释命令,并提交操作系统执行
本地方法接口:融合不同的编程语言为Java所用,通常为C/C++
垃圾收集器:
二、运行时数据区
虚拟机栈:每执行一个方法就会有该方法的一个栈帧入栈,执行完出栈,遵循“先入后出”,主方法栈帧总是最后出栈
本地方法栈:本地方法库中的方法(不是java代码)
堆:存对象,字符串常量池
程序计数器:
元数据区(方法区):类信息
“栈管运行 堆管存储”
1、虚拟机栈
每个java方法在被调用的时候就会创建一个栈帧,并入栈,一旦方法调用结束,就出栈。其生命周期是随着线程的创建而创建,随线程结束而释放,是线程私有的,因此不存在垃圾回收的问题。遵循“先进后出”原则。
以一个java程序为例子解释一哈:
public class Test {
public static void main(String[] args) {
doit1();
doit3();
}
public static void doit1() {
doit2();
}
public static void doit2() {
//方法体
}
public static void doit3() {
//方法体
}
}
稍作解释:一个java程序入口为主方法,因此一定主方法的栈帧先入栈,调用doit1()时,doit1()栈帧入栈,在doit1()中调用了doit2(),doit2()方法入栈,执行完后,出栈,继续执行doit1()方法,doit1()方法执行完毕,出栈,继续执行main方法,main方法调用doit3(),doit3栈帧入栈,执行完后出栈,继续main方法,main方法执行完毕,该程序执行完毕。
栈帧:
栈帧中主要是:局部变量表、操作数栈、动态链接、返回地址
局部变量表主要存放:byte、short、int、float、char、boolean、reference(指的是方法中的局部变量的对象引用)、returnAdress(虚拟机数据类型)
- 单位是slot,long和double型变量占2个slot
- 通过索引定位的方式使用局部变量表,索引值范围从0开始
- 在方法的执行过程中,使用局部变量表完成参数值到参数列表的传递过程
- 局部变量是没有初始值的
操作数栈:
- 遵循“先进后出原则”,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程
- 操作数栈的每个元素是可是是任意的数据类,32位数据类型所占栈容量为1,64位数据类型所占的栈容量是2
- 当一个方法开始执行时,这个方法的操作数栈数空的;在方法的执行过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中;当方法执行完后,操作数栈为空。
动态链接:
- 每个栈帧都包含一个执行运行时常量池(JVM运行时数据区域)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接
- 在Class文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1、类的权限定名 2、字段名个属性 3、方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用,这里称为静态解析。另外一部分将在每一次的运行期间转换为直接引用,这部分呢称为动态链接。
方法返回地址:
- 当一个方法开始执行后,只有两种方法可以退出这个方法。一,执行引擎遇到任意一个方法的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值类型将根据遇到何种方法返回指令来决定,这种退出方式称为正常完成出口;二:在方法的执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是java虚拟机内部产生的异常,还是代码中使用抛出字节码指令而产生的异常,只要在本方法中的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出称为异常完成出口,该退出方式是不会给上层调用者任何返回值。
- 无论采用哪种退出方式,在方法退出后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的程序技术器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不保存这个信息、
- 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。
均是参考的一个写的很好的简书:
https://www.jianshu.com/p/ecfcc9fb1de7
当虚拟机栈满了之后,会出现java.lang.StackOverflowError
public static void main(String[] args) { doit(); } public static void doit() { doit(); }
2、堆
Java程序在运行时创建的所有类型对象个数组都存储在堆中。JVM会根据new指令在堆中开辟一个确定类型的的对象内存空间,堆中开辟对象的空间并没有任何人工指令可以回收,而是通过JVM的垃圾回收器负责回收。
- 堆中对象存储的是该对象以及对象所在超类的实例数据(但不是静态数据)
- 其中一个对象的引用可能在整个运行时数据区中的很多方法存在,比如栈、堆、方法区等
- 堆中对象还在关联一个对象的锁数据信息以及线程的等待集合(线程等待池),这些都是实现Java线程同步机制的基础
- java中数组也是对象,在堆中会存储数组的信息
堆内存如下:
新生代:是new一个对象诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集,结束生命。伊甸区,所有的对象都在此被new出,当伊甸区的空间用完,而程序还在创建对象,JVM的垃圾回收器对其进行垃圾回收(Minor GC),将伊甸区不再所引用的对象进行销毁,然后将剩余的移动到幸存0区,当幸存0区满了,JVM垃圾回收器也会对其进行垃圾回收(Minor GC),移动到幸存1区,依次类推。若老年代(养老区)满了,JVM对其执行Full GC垃圾回收,若依旧无法保存对象,则会发生OOM异常(OutOfMemoryError)
发生OOM异常的原因:
- JAVA虚拟机的堆内存设置不够,可以通过-Xmx、-Xms进行调整
- 代码中创建了大量对象,且长时间不能不能进行回收
public static void main(String[] args){ long maxMemory = Runtime.getRuntime().maxMemory();//返回JVM试图使用的最大内存量 long totalMemory = Runtime.getRuntime().totalMemory();//返回JVM中的内存总量 System.out.println("MAX_MEMORY = " + maxMemory + "(字节)" + (maxMemory/(double)1024/1024) + "MB"); System.out.println("TOLAL_MEMORY = " + totalMemory + "(字节)" + (totalMemory/(double)1024/1024) + "MB"); } //输出: MAX_MEMORY = 937951232(字节)894.5MB TOLAL_MEMORY = 64487424(字节)61.5MB
将堆内存调成800M:-Xmx800m -Xms800m -XX:+PrintGCDetails
//输出 MAX_MEMORY = 804257792(字节)767.0MB TOLAL_MEMORY = 804257792(字节)767.0MB
模拟下堆内存溢出(OOM),将对内存调成8M:-Xmx8m -Xms8m -XX:+PrintGCDetails
执行如下代码
public static void main(String[] args){ String str = "模拟堆内存溢出"; while(true) { str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999); } }
则会出现OOM异常:
这是在执行上述代码时,垃圾回收的一个过程:
3、垃圾回收机制
VM参数:-Xmx1024m -Xms1024m -XX:+printGCDetails
- -Xms:设置初始分配大小,默认为物理内存的1/64
- -Xmx:设置最大分配内存,默认为物理内存的1/4
- -XX:+printGCDetails:输出详细GC处理日志
GC:
- 频繁收集新生代
- 较少收集老年代
- 基本不动perm区
Minor-GC:普通GC,只针对新生代
Major-GC或者Full-GC:针对老年代,偶尔伴随对新生代的GC以及对永久代的GC
一般是自动出发垃圾回收,主要的垃圾回收算法:
- 复制算法
- 标记-清除算法
- 标记-整理算法
- 分代收集算法
新生代:当对象在Eden(包括一个Survior区域,这里为from区)出生后,在经过一次Minor GC后,如果对象还存活,并且能被另外一块Survior区所容纳(to区),即to区有足够的内存空间存储Eden和from区的存活对象,则使用复制算法将这些存活的对象复制到另一块Survior(to区),然后清理所有使用的Eden以及from区,并且将这些对象的年龄设置为1,以后对象在survior区每熬过一次Minor GC,就将其年龄加1,当年龄达到某个值时(一般为15),这些对象就会成为老年代。
Eden区的对象的存活率不高,一般使用10%的内存作为空闲和活动区,而另外80%的内存,则用来给新new的对象分配内存,一旦发生GC,将10%的活动区间与另外80%中存活的对象转移到10%的空闲空间,然后用90%的内存将被释放,依次类推,
Eden:from:to = 8:1:1. 谁空谁为to区
复制算法:将内存分为两块,每次只用其中一块,当这块内存用完,就将还活着的对象复制到另外一块。无论如何有一个区是空的,谁空谁就是to区。
优点:不会产生碎片,完整度高。
缺点:会浪费空间
老年代:一般是标记清除法和标记整理的混合实现
标记清除算法:从根集合开始扫描,对存活的对象进行标记,然后扫面整个内存空间,回收未被标记的对象,
优点:不需要内存空间
缺点:有碎片,且要经历两次扫描,耗时严重
标记整理算法:先标记活的对象,然后整理使其内存连续,再将未被标记的对象清除
优点:不会产生碎片
缺点:效率不高,不仅要标记,还有整理活着的对象的引用地址,将其变为连续。从效率上讲,该算法低于复制算法。
分代收集算法:各种算法的混合使用。
未完待续。。。。。。。。。。。。