一:java代码的执行流程(引出JVM)
- 首先由程序员编写成.java文件
- 然后由javac(java编辑器)将.java文件编译成.class文件
- .class文件可以在不同平台/操作系统上的JVM上执行
- 再由JVM编译成可供不同操作系统识别的机器码(0,1二进制)
二:JVM来源
我们在下载JDK的时候,也就把JVM给下载下来了 (因为JDK包含JRE,JRE包含JVM)
1:JRE: Java Runtime Environment
JRE顾名思义是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
2:JDK:Java Development Kit
顾名思义是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
3.JVM虚拟机
JVM(JAVA虚拟机)是运行Java字节码的虚拟机,通过编译.java文件为.class文件得到字节码文件 . .class文件包含JVM可以理解的字节码。
在现实世界中,JVM是一种规范,它提供可以执行Java字节码的运行时环境。
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
三:JVM在Java程序运行当中的位置
JVM之所以称为虚拟机,是因为它提供了一个不依赖于底层操作系统和机器硬件体系结构的机器接口。这种与硬件和操作系统的独立性使得Java程序“写一次,到处运行”(write-once-run-anywhere).
四:JVM的体系结构
1:图示
那么我们分层来解析这个体系结构的重要的部分
2:类加载器系统
(1):类加载机制
a:首先要知道 java程序运行的几个阶段
b:累加载机制
-
类加载顾名思义就是把类加载到 JVM 中,而输出一段二进制流到内存,之后经过一番解析、处理转化成可用的 class 对象,这就是类加载要做的事情。
-
二进制流可以来源于 class 文件,或者通过字节码工具生成的字节码或者来自于网络都行,只要符合格式的二进制流,JVM 来者不拒
-
类加载流程分为加载、连接、初始化三个阶段,连接还能拆分为:验证、准备、解析三个阶段。
-
加载:JVM在该阶段的主要目的使将字节码从不同的数据源(可能是class文件,也可能是jar包,甚至使网络)转化为二进制字 节流加载到内存中方法区中,并在内存中生成一个代表该类的Class对象
-
验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前 JVM 版本等等之类的验证。
-
准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如 int 的初始值是 0。
-
解析:将常量池的符号引用转化成直接引用。符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个 A 去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是小明去做一件事。 解析就是把 A(符号引用) 替换成小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。
-
初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。
c:类加载的补充
那么这个类加载中 加载阶段 我们从不同的数据源找这个.class文件是如何找到的呢?
-加载类,JVM有三种类加载方式:Bootstrap(启动类加载器),extension(扩展类加载器),application(系统类加载器)
-当加载类文件时,JVM会找到某个任意类XYZ.class的依赖项。
- 第一个启动类载入器试图查找类,它会扫描lib文件夹下的rt.jar文件
- 如果没有找到类,则extension类加载器会在jre\lib\ext文件夹下查找该类
- 同样没有找到类,application类加载器会在系统CLASSPATH环境变量中查询所有的Jar文件和类.
- 如果类被任何加载器发现,则被类加载器载入,否则抛出异常:ClassNotFoundException。
(2):类加载器
首先这个说到类加载器肯定是要讲到双亲委派模型的
- 类加载器:讲到类加载不得不讲到类加载的顺序和类加载器。Java 中大概有四种类加载器,分别是:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),系统类加载器(System ClassLoader),自定义类加载器(Custom ClassLoader),依次属于继承关系(注意这里的继承不是 Java 类里面的 extends)
- 启动类加载器(Bootstrap ClassLoader):主要负责加载存放在Java_Home/jre/lib下,或被-Xbootclasspath参数指定的路径下的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载),启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器(Extension ClassLoader):主要负责加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载Java_Home/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 系统类加载器(System ClassLoader):主要负责加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器(Custom ClassLoader:自己开发的类加载器
- 双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。
为甚要双亲委派
其实就是规范,当我们加载这个.Class字节码文件的时候,是从本地磁盘中找这个文件的,那么入如果有在不同文件目录下的相同的.Class文件的时候 那就该如何选择呢,这时就需要一定的规范的和准则了。
它使得类有了层次的划分。就拿 java.lang.Object 来说,加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar里面的java.lang.Object加载到JVM中。
这样如果有不法分子自己造了个java.lang.Object,里面嵌了不好的代码,如果我们是按照双亲委派模型来实现的话,最终加载到JVM中的只会是我们rt.jar里面的东西,也就是这些核心的基础类代码得到了保护。
因为这个机制使得系统中只会出现一个java.lang.Object。不会乱套了。你想想如果我们JVM里面有两个Object,那岂不是天下大乱了。
3:JVM的内存空间
(1):前言
主要是考虑jvm的内存和实际物理内存的关系,首先可以知道的jvm(虚拟机)的内存模型是虚拟出来的模仿实际操作系统内存的,但我们new 出来一个对象是占用虚拟机的内存,同时也占用物理内存(虚拟机中的内存是与计算机的物理内存映射的)。
(2):JVM的体系结构图
(3):操作系统的内存布局
a:前言
为了解JVM的体系结构图,我们非常有必要先了解一下操作系统的内存基本结构
b:操作系统的内存布局
c:操作系统的JVM
为什么jvm的内存是分布在操作系统的堆中呢??因为操作系统的栈是操作系统管理的,它随时会被回收,所以如果jvm放在栈中,那java的一个new的对象就很难确定会被谁回收了,那gc的存在就一点意义都没有了,而要对栈做到自动释放也是jvm需要考虑的,所以放在堆中就最合适不过了。
d:操作系统+jvm的内存简单布局
- 从这个图,你应该不难发现,原来jvm的设计的模型其实就是操作系统的模型,
- **基于操作系统的角度,jvm就是个该死的java.exe/javaw.exe,也就是一个应用,用来加载.class文件
- 而基于class文件来说,jvm就是个操作系统,**
- 而jvm的方法区,也就相当于操作系统的硬盘区,
- 而java栈和操作系统栈是一致的,无论是生长方向还是管理的方式,
- 至于堆嘛,虽然概念上一致目标也一致,分配内存的方式也一直(new,或者malloc等等),但是由于他们的管理方式不同,jvm是gc回收,而操作系统是程序员手动释放,所以在算法上有很多的差异,gc的回收算法。
e:操作系统+jvm的内存简单布局+PC寄存器
将这个图和上面的图对比多了什么?没错,多了一个pc寄存器,我为什么要画出来,主要是要告诉你,所谓pc寄存器,无论是在虚拟机中还是在我们虚拟机所寄宿的操作系统中功能目的是一致的,计算机上的pc寄存器是计算机上的硬件,本来就是属于计算机,(这一点对于学过汇编的同学应该很容易理解,有很多的寄存器eax,esp之类的32位寄存器,jvm里的寄存器就相当于汇编里的esp寄存器),计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址,它甚至可以是操作系统指令的本地地址,当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined,所以你现在应该很明确的知道,虚拟机的pc寄存器是用于存放下一条将要执行的指令的地址(字节码流)。
f:操作系统+jvm的内存简单布局+PC寄存器+classLoader
- 这个图是要告诉你,当一个classLoder启动的时候,classLoader的生存地点在jvm的堆,
- 然后它会去主机硬盘上将A.class装载到jvm的方法区,
- 方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,
- 然后A字节码这个内存文件有两个引用一个指向A的Class对象,一个指向加载自classLoader
- 那么这个字节码文件中还有什么信息呢
补充:
这里的A.class文件存放的位置 主机磁盘,就是我们类加载阶段中的加载 通过不同的类加载器
在主机不同的文件夹中找.class
(4):JVM内存中的栈和堆
a:Java内存分配中的栈
在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
b:Java内存分配中的堆
-
堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
-
在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。
-
引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍 然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。
-
实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!
4:JVM的垃圾回收器
JVM笔记之垃圾回收器