前面已经分析过JVM内存区域的各个功能,可是过于抽象和散乱,接下来就根据Math.java、Math.class、JVM结构图和Java VisualVM工具来完整详细的理解一个代码在JVM中是如何执行的,以及JVM是如何对内存区域进行垃圾收集的。java
首先,Math.java文件是咱们用编译器编写的源代码文件,可是,这种文件人看得懂机器看不懂,因此Math.java文件要转换成字节码文件Math.class,也就是二进制文件;而后把字节码文件Math.class传进JVM让机器进行编译处理,最后把代码编译成机器码,实现对机器的控制。
如图所示,不一样的文件对应不一样的编译器,最终都会转换成字节码文件,而后才能传入JVM:
以下图所示:字节码文件翻译成机器码还有三个步骤
程序员
public class Math { public static final int initData = 666; public static User user = new User(); public int compute(){ int a=1; int b=2; int c=(a+b)*10; return c; } public static void main(String[] args) { Math math=new Math(); math.compute(); System.out.println("test"); } }
(1)先执行main方法,而后找到target目录下已经编译好的Math.class文件,在控制器中打开
(2)此时虽然已经获得class文件,可是文件里都是字节码,不方便阅读,因而使用命令进行反编译,获得一个咱们容易阅读的代码内容
(3)在控制器中获得咱们想要的字节码文件内容web
public class com.itheima.Math { public static final int initData; public static com.itheima.User user; public com.itheima.Math(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int compute(); Code: 0: iconst_1 //把int类的常量1压入操做数栈---1; 1: istore_1 //把int类的值存入局部变量1---int a=1; 2: iconst_2 //把int类的常量2压入操做数栈---2; 3: istore_2 //把int类的值存入局部变量2---int b=2; 4: iload_1 // 5: iload_2 6: iadd 7: bipush 10 9: imul 10: istore_3 11: iload_3 12: ireturn public static void main(java.lang.String[]); Code: 0: new #2 // class com/itheima/Math 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method compute:()I 12: pop 13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 16: ldc #6 // String test 18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 21: return static {}; Code: 0: new #8 // class com/itheima/User 3: dup 4: invokespecial #9 // Method com/itheima/User."<init>":()V 7: putstatic #10 // Field user:Lcom/itheima/User; 10: return }
完成上面的材料准备工做,就能够跟着代码来分析执行的流程了!!!算法
Math.class文件最开始放在磁盘中,而后通过【类装载子系统】装入【方法区】,字节码执行引擎读取方法区的字节码自适应解析,边解析边运行,而后PC寄存器指向了main函数所在的位置,虚拟机开始在栈中为main函数预留一个栈帧,而后开始运行main函数,main函数里的代码被字节码执行引擎映射成本地操做系统里相应的实现,而后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,而后运行本地方法,调用操做系统APIi等等。spring
上图代表:jvm 虚拟机位于操做系统的堆中,而且,程序员写好的类加载到虚拟机执行的过程是:当一个 classLoder 启动的时候,classLoader 的生存地点在 jvm 中的堆,而后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),而后在堆内存生成了一个A字节码的对象,而后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载本身的classLoader。数据库
类加载器把加载到的全部信息放入方法区,也就是说方法区里存放了类的信息,有类的static静态变量、final类型变量、field自动信息、方法信息,处理逻辑的指令集,咱们仔细想一想一个类里面也就这些东西。数组
而堆中存放是对象和数组,
1-这里的对应关系就是 “方法区–类” “堆–对象”,以“人”为例就是,堆里面放的是你这个“实实在在的人,有血有肉的”,而方法区中存放的是描述你的文字信息,如“你的名字,身高,体重,还有你的行为,如吃饭,走路等”。
2-再者咱们从另外一个角度理解,就是从前咱们得知方法区中的类是惟一的,同步的。可是咱们在代码中每每同一个类会new几回,也就是有多个实例,既然有多个实例,那么在堆中就会分配多个实例空间内存。tomcat
方法区的内容是边加载边执行,例如咱们使用tomcat启动一个spring工程,一般启动过程当中会加载数据库信息,配置文件中的拦截器信息,service的注解信息,一些验证信息等,其中的类信息就会率先加载到方法区。但若是咱们想让程序启动的快一点就会设置懒加载,把一些验证去掉,如一些类信息的加载等真正使用的时候再去加载,这样说明了方法区的内容能够先加载进去,也能够在使用到的时候加载。多线程
方法区是被字节码执行引擎直接执行的,因此静态信息都是在类加载的时候就建立了的,非静态的信息是在堆内存实例化对象的时候才建立,因此静态方法中不能调用非静态的方法和变量
jvm
(1)在以前学习Static关键字的时候了解到,Math math=new Math();,在这个代码执行时分为三块:
(2)咱们把栈放大,能够看到在栈里面,每个方法都有一个本身的栈帧,main方法有一个栈帧,compute方法有一个栈帧,在代码中main方法还调用了compute方法,那么它们是怎么存放信息、处理信息并相互传递信息的呢?
由于先运行main方法,因此main方法先入栈,再调用compute方法,再入栈,由于栈的特色是先入后出,因此compute先出栈,接下分析compute的结构和信息处理过程。
方法栈帧的内存又能够分为4个部分:局部变量表、操做数栈、动态连接、方法出口。其中局部变量表用来存放变量的名称a、b、c等,操做数栈存放要进行赋值的操做数一、2。操做流程以下:
把操做数栈里的值赋值给局部变量表中的变量,还有其余一系列操做,操做结果就获得了 c=30,而且使用return语句把结果返回了,可是这个值是怎么传递给main方法的呢?那就是经过方法出口,把结果传递给栈中的下一个方法(方法栈帧是按照调用顺序入栈的,因此结果都是顺着往下传递),此时compute方法栈帧完成出栈。
再来分析main方法栈帧,它一样也包含局部变量表
从上面的分析能够看出,经过栈和命令使得方法完成了数据的赋值和处理,还有方法的调用和处理等操做,最后把方法的处理的结果经过方法出口传递给调用者。
在Math.class文件中能够看到,每一行指令都有一个行数,若是线程在执行指令的时候忽然被挂起了,等线程被唤醒后要接着上次被挂起的位置执行怎么办?
那就要给每个方法线程配备一个程序计数器,用来标记线程执行的位置,每执行一行指令,字节码执行引擎都会命令程序计数器更新到最新的行数。
因此程序计数器的做用为:
在main方法中对Math进行实例化,栈内存中的math地址指向堆内存中的math对象,可是math对象对应的那些属性和方法不会也都放在堆内存中,这就要讲到下一个区域方法区(元空间)了
类加载器加载的类信息放到方法区---->执行程序后,方法区的方法压入栈的栈顶---->栈执行压入栈顶的方法---->遇到new对象的状况就在堆中开辟这个类的实例空间。(这里栈是有此对象在堆中的地址的)
堆分为:新生代、老年代
新生代分为:Eden区、From Survivor区、To Survivor区
全部新建立的对象都会放在堆内存的Eden区里,这个区最大,当Eden的空间用完了,程序又须要建立对象,就会使用minor GC算法对新生代进行垃圾收集,Eden区中死亡的对象被垃圾收集器处理掉,存活的对象进入From Survivor区;若是From Survivor区也满了,再进行minor GC算法,还活着的对象从From Survivor区进入To Survivor区,而且对象年龄+1,接下来每次有存活的对象就在From Survivor区、To Survivor区两个区里来回跑。
当对象的年龄达到15时,说明这个对象真的很难死亡,那么就能够把这个对象传入老年代,在老年代中若是内存也被放满了就会使用full GC进行垃圾收集
JDK为咱们提供了监测工具VisualVM,咱们直接在 cmd 中输入 jvisualvm 命令就能够进入
可是工具VisualVM是没有安装Visual GC的插件的,安装Visual GC的插件参考博客:安装Visual GC的插件
public class HeapTest { byte[] a=new byte[1024*100]; public static void main(String[] args) throws InterruptedException { ArrayList<HeapTest> heapTests=new ArrayList<>(); while (true){ heapTests.add(new HeapTest()); Thread.sleep(10); } } }