快速入门JVM,只看这一篇就够了
1.JVM的整体结构
在运行时数据区中,方法区和堆区是线程共享的,而其他区域是线程独占的,这一点要注意。接下来,会有堆JVM的各个结构做更加深入的讲解。
2.回顾一下Java代码的执行流程
从宏观上看,Java源程序会被编译成字节码文件,然后字节码文件会在不同操作系统上的JVM上被执行,从而得到我们想要的结果。
从微观上看,会有很多复杂的过程,这篇博客写得非常清楚,推荐给大家:https://blog.csdn.net/sinat_33087001/article/details/76977437?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-tas
3.JVM的生命周期
(1)虚拟机的启动
Java虚拟机的启动时通过引导类加载器(Bootstrap Class Loader)创建一个初始类(Initial Class)来完成的,这个类是由虚拟机的具体实现指定的。
(2)虚拟机的执行
一个运行中的Java虚拟机有一个清晰的任务:执行Java程序
程序开始执行时才运行,程序结束时就停止
执行一个所谓的Java程序时,真正执行的是一个叫做JVM的进程
(3)虚拟机的退出
有如下几种情况:
程序正常执行结束
程序在执行过程中遇到了异常或错误而终止
由于操作系统出现错误而导致JVM进程终止
某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
JNI规范描述了用JNI Invocation API来加载或卸载JVM时
4.类加载器子系统
类加载器子系统负责从文件系统或网络中加载Class文件,Class文件在文件开头有特定的文件标识
ClassLoader只负责class文件的加载,至于它是否可以运行,则有Execution Engine决定
加载的类信息存放于一块称为方法区的内存空间,除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)
注意到:虚拟机自带的加载器:
引导类加载器(bootstrap),底层是C++,出厂就有的,即从1.0版本开始就有,像Object、String类就由此加载器加载
扩展类加载器(Extension),由Java扩展,为满足日益增长的需要
应用程序类加载器(AppClassLoader),Java,也叫做系统类加载器,加载当前应用的classpath的所有类
此外,还有用户自定义加载器,为Java.lang.ClassLoader的子类,用户可以自定义类的加载方式。
可以用下面的图来表示几个类加载器之间的关系
为什么会有向上指的指针呢?这就要牵扯到一个重要的机制:双亲委派机制。这是啥意思?用一个案例来解释。假设我在src目录下建立了一个java.lang包,然后在该包了定义了一个类String,然后编写main方法,里面输出“hello world”,语法上没有任何错误,但程序就是启动不来。这是为啥?原因是双亲委派机制保证了Java体系的安全性,即类加载器要加载这个自定义的String类,要先从Bootstrap ClassLoder开始加载起,如果Bootstrap ClassLoader找到了java.lang.String,就加载这个类,很显然这个类是Java的rt.jar包中的,立马就找到了;因此也就轮不到System ClassLoader来加载我自定义的这个String类了。
下面给出总结:
当一个类收到了类加载请求,它首先不会尝试自己加载这个类,而是把这个请求委派给父类完成,每一个层次类加载器都是如此,因此所有的加载请求都是传送到Bootstrap类加载器中,只有当父类加载器反馈自己无法完成这个请求时(即在它的加载路径下没有找到所需要加载的Class),子类加载器才会尝试去加载。采用双亲委派的一个好处就是,如加载rt.jar包中的类java.lang.Object时,不管是哪个加载器加载这个类,最终都是委托给顶层的Bootstrap ClassLoader进行加载,这样就保证了使用不同的类加载器最终得到的都是同一个Object对象。
5.Native,本地方法区/本地方法接口
普通的类当中不能有只声明为实现的方法,但是可以有用native修饰的只声明未实现的方法。
无法通过编译,报错:
用native修饰可以完成:
声明了native的方法就是调用底层操作系统或者C/C++的函数了,与Java没有任何关系了。
Native Interface本地接口:
本地接口的作用是融合不同的编程语言为Java所有,Java调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载Native Libraies.
Native Method Stack
具体做法就是Native Method Stack中等级native方法,在Execution Engine执行时加载本地方法库。
6.PC寄存器,也即程序计数器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记,
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果执行的是一个Native方法,那这个计数器是空的。用以完成分支,循环,跳转,异常处理,线程恢复等基础功能,不会发送内存溢出(Out Of Memory)错误。
7.方法区(Method Area,线程共享,存在垃圾回收)
各线程共享的运行时内存区域,它存储了每一个类的结构信息,例如运行时的常量池、字段和方法数据、构造函数和普通方法的字节码内容。不同虚拟机的实现是不一样的,最典型的就是永生代(PermGen Space)和元空间(Metaspace)。
但是,实例变量存在堆内存中,与方法区无关!
各线程共享的运行时内存区域,它存储了每一个类的结构信息,例如运行时的常量池、字段和方法数据、构造函数和普通方法的字节码内容。不同虚拟机的实现是不一样的,最典型的就是永生代(PermGen Space)和元空间(Metaspace)。
但是,实例变量存在堆内存中,与方法区无关!
8.栈区(Stack Area)
总的一句话,栈管运行,堆管存储。
栈内存,主管Java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期的,线程结束时栈内存就释放,不存在垃圾回收的问题,且为线程私有的。
8种基本数据类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配的。
栈帧中主要保存3类数据:
本地变量:输入参数和输出参数以及方法内的变量
栈操作:记录出栈、入栈的操作
栈帧数据:包括类文件、方法等
当Java中的方法,被JVM处理时,其就成为了栈帧
以下图片更加方便理解:
如果递归调用没有递归的结束条件,就会抛出Stack Overflow Error错误。
重要:栈+堆+方法区的交互关系:
在HotSpot虚拟机中,是使用指针的方式来访问对象:Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址。
比如Student std=new Student(),std存放在栈区,Student对象存放在堆区,用来造Student对象的模板在方法区,三者之间在HotSpot虚拟机中通过指针来引用。
9.堆区(Heap)
在JDK7之前,堆内存逻辑上分为:新生区+养老区+永久区
JDK8以后,堆内存逻辑上分为:新生区+养老区+元空间