快速入门JVM
Posted 奥利奥的笔记
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速入门JVM相关的知识,希望对你有一定的参考价值。
本文将介绍运行时数据区的java栈(java stack)和堆(heap)。介绍栈管运行,堆管存储。介绍栈、堆、方法区的交互。简单介绍GC的发生。
序
在数据结构中,队列和栈是必须要掌握的。队列最显著的特点是先进先出(FIFO)。栈的特点是先进后出(FILO)。
栈(Stack)
栈也叫栈内存,主管java程序运行,是在线程创建时创建,他的生命周期跟随线程的生命周期,线程结束占栈内存就释放。栈内存是线程私有。对于栈来说不存在垃圾回收问题。在栈内存中会分配8种基本类型的变量、对象的引用变量和实例方法。
栈的大小和具体的JVM实现有关。
栈帧
栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个有关方法和运行数据的数据集。每一次调用函数,都会在调用栈上维护一个的独立的栈帧。一个栈帧一般包括:
本地变量:包括输入参数、输出参数以及方法内的变量;
栈操作:入栈、出栈操作;
每一个方法从调用直至执行完毕的过程,对应着一个栈帧在虚拟机中入栈到出栈的过程。
栈帧的应用过程
执行下列代码:
public class testStackFrame {
public void method2(){
System.out.println("栈帧2建立。。");
System.out.println("栈帧2入栈。。");
System.out.println("方法2执行。。");
System.out.println("方法2执行完成。。");
System.out.println("栈帧2出栈。。");
}
@Test
public void method1() {
System.out.println("栈帧1建立。。");
System.out.println("栈帧1入栈。。");
method2();
System.out.println("方法1继续执行。。");
System.out.println("方法1执行完成。。");
System.out.println("栈帧2出栈。。");
}
}
输出如下:
对应到栈的操作如下图所示,在一个栈中有两个栈帧,具体过程是:
栈帧1是被最先调用的方法,先入栈;
方法1调用了方法2,栈帧2入栈处于栈顶的位置;
方法2执行完毕,栈帧2弹出;
方法1等调用的方法2执行完毕之后,继续执行完成,栈帧1弹出;
线程结束,栈释放。
StackOverFlowError
执行下列代码:
public class TestStackOverFlowError {
public void method2(){
System.out.println("栈帧2建立。。");
System.out.println("栈帧2入栈。。");
System.out.println("方法2执行。。");
method2();
System.out.println("方法2执行完成。。");
System.out.println("栈帧2出栈。。");
}
@Test
public void method1() {
System.out.println("栈帧1建立。。");
System.out.println("栈帧1入栈。。");
method2();
System.out.println("方法1继续执行。。");
System.out.println("方法1执行完成。。");
System.out.println("栈帧2出栈。。");
}
}
会报一个如下图的错误,原因是method2不断调用method2,不断建立栈帧,超出了java栈的空间大小,发生error。
栈、堆、方法区的交互关系
JVM具体执行流程
看如下代码:
public class TestJVM {
public static void main(String[] args) {
Chinese chinese1 = new Chinese("奥利奥", 24, 6000.0, '男');
Chinese chinese2 = new Chinese();
System.out.println(chinese1.getAge());
}
}
系统启动一个java虚拟机进程,这个进程首先加载TestJVM.class文件,读取这个文件中的二进制数据,然后把TestJVM这个类的类信息放到运行时数据区的方法区。即类的加载。
接着,JVM定位到TestJVM中的main(),开始执行他的指令。main()的第一句是
Chinese chinese1 = new Chinese("奥利奥", 24, 6000.0, '男');
就是让JVM创建一个Chinese实例,使用chinese1来引用这个实例。
JVM一看,要建立实例了,直接去方法区(方法区存放了已加载类的结构信息)找,这时候一定找不到,因为还没加载Chinese,既然没加载,那就加载吧,于是把Chinese的类信息存放到方法区。
JVM继续执行下一条指令,在堆区继续创建另一个类Chinese的实例,然后执行打印操作。当JVM执行chinese1.getAge() ,JVM根据局部变量chinese1持有的引用,定位到堆中类Chinese的实例,再根据类Chinese的实例持有的引用,定位到方法区类Chinese的结构信息,从而获取到getAge()成员方法的字节码,接着执行该成员方法包含的指令。
堆(Heap)
一个JVM实例只有一个堆内存,所有线程共享。堆内存大小可以调节。类加载器读取了类文件之后,需要把类、方法、常量、变量放到堆内存中,保存所有引用类型的真实信息,方便执行器执行。
堆内存分为如图三部分:
新生区
新生区是类的诞生、成长、消亡的区域。一个类在这路产生、应用,最后被垃圾回收器收集,结束生命。
新生区又分为两部分:伊甸园(Eden)区和幸存者(Survivor)区。所有类都是在伊甸园区被new出来;幸存者区又分幸存0区(S0或者from区)和幸存1区(S1或者to区)。
新生区占堆内存1/3。其中伊甸园区占8/10,S0和S1各占1/10。
Minor GC、Full GC和OOM
当伊甸园区的空间被用完时,程序又需要创建对象,这时候就会触发JVM的垃圾回收器对伊甸园区进行垃圾回收。这里的垃圾回收指的是轻量级的垃圾回收(Minor GC)。会将伊甸园区中不被其他对象所引用的对象进行销毁。然后将剩余对象移动到幸存0区。
若S0也满了,再对该区进行垃圾回收,然后将剩余移动到S1。
若S1也满了,就移动到养老区。
若养老区也满了,会进行对养老区的垃圾回收,这里的垃圾回收是Full GC。
若多次Full GC之后,依然无法进行对象的创建保存,就会产生OOM异常。
Java heap space
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明java虚拟机堆内存不够,原因有二:
一是java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整;
二是代码中创建了大量大对象,并且长时间不能被垃圾收集器回收(即存在被引用)。
Minor GC的过程
Minor GC的过程就是复制->清空->互换的过程。
首先,当伊甸园区满的时候触发第一次GC,把还活着的对象拷贝到from区;
当伊甸园区再一次触发GC时,会扫描伊甸园区和from区,对这两个区域进行垃圾回收,经过这次回收还活着的对象,会直接复制到to区,并把这些对象年龄加1。如果这时候有对象的年龄达到老年的标准,则复制到老年区;
然后清空伊甸园区和from区中的对象,即复制之后清空;
最后,to区和from区互换,原to区称为下一次GC时的from区,即谁空谁是to区;
部分对象会在from和to之间复制来复制去。如此交换15次之后,最终如果还存活,就存入到老年区。
15次由JVM参数MaxTenuringThreshold决定,该参数默认为15。
简单讲就是,将eden,from复制到to;清空eden,from;from 和to互换。
永久代
永久代对应的是是方法区。在上一个文章里说过,方法区是一种规范,在jdk7 之前的实现是永久代,在jdk8之后实现是元空间。
实际讲,方法区和堆一样,是所有线程共享的内存区域。虽然JVM将方法区描述为堆的一个逻辑部分,但他却还有一个别名叫Non-heap(非堆),目的就是和堆分开。
永久存储区是一个常驻内存区域,用于存放jdk自身所携带的类和接口的元数据,存储的是运行环境必须的类信息,该部分区域的数据是不会被垃圾回收器回收的,只有关闭JVM才会释放此区域占用的内存。
20191124
以上是关于快速入门JVM的主要内容,如果未能解决你的问题,请参考以下文章