JVM进一步探究

1.栈和堆的管理

java自动管理堆和栈,程序员不能直接设置堆和栈。

2.为什么JVM的内存是分布在操作系统的堆中呢?

因为操作系统的栈是操作系统管理的,它随时会被回收,所有如果JVM放在了操作系统的栈中,那java的一个null对象就很难确认会被谁回收了,那gc的存在久一点意义也没有了,而且要对栈做到自动释放也是JVM需要考虑的,所以放在堆中再适合不过了。

3.操作系统的栈和堆

栈(操作系统) 一般由程序员分配释放,若程序员不是放,程序结束时可能由OS回收,分配方式类似链表。
堆(操作系统) 由操作系统自动分配释放,存放函数的参数值,局部变量等。操作方式与数据结构中的栈相似。

4.类加载到虚拟机的过程

JVM虚拟机位于操作系统的堆中,并且,程序员写好的类加载到虚拟机执行的过程是:当一个classloader启动的时候,classloader的生存地点在JVM的堆中,然后它回去主机硬盘将字节码文件加载到jvm的方法区,方法区中的这个字节码文件会被虚拟机拿来,new一个字节码对象,然后堆中就有了对应的字节码对象,因此,这个字节码内存文件由两个引用,一个指向字节码对象,一个指向加载自己的classloader。

5.java虚拟机的生命周期

生命周期的起点时当一个java应用main函数启动时,虚拟机也被启动,而中有当虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才会结束生命。

6.java虚拟机与main方法的关系

main方法是java应用的入口,被执行时,java虚拟机就启动了,启动几个main就有几个应用,几个虚拟机。

7.java虚拟机的两种线程

一种叫守护线程,一种叫普通线程,main方法是一个普通线程,虚拟机的gc时一个守护线程,java虚拟机中,只要由任何普通线程还没有结束,java虚拟机实例就不会退出。

8.gc

垃圾回收机制,一种经典的守护线程
垃圾回收机制不是创建的变量为空就立刻回收,而是超出变量的作用域后被自动回收。

9.程序在JVM执行的流程

首先,当一个程序启动之前,他的class会被classloader装入方法区,执行引擎读取方法区的字节码自适应解析,边解析边运行,然后pc寄存器指向main方法所在位子,虚拟机开始为main方法在栈中预留一个栈帧,然后开始执行,main方法中的代码会被执行引擎映射成/本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操作系统会为本地方法分配本地方法栈,用来存储一些临时变量,然后运行本地方法,调用操作系统api等等。.

10.JVM内存

JVM内存包含两个子系统和两个组件,两个子系统是:Classloader子系统和Executionengine(执行引擎),两个组件是:运行时数据区域组件和本地库接口组件。

11.什么是本地库接口和本地方法库

①.本地方法库接口:即操作系统所使用的编程语言的方法集,是归属于操作系统的。
②本地方法库保存在动态链接库中,格式是各个平台专有的。

12.双亲委派机制

JVM加载类时默认采用的是双亲委派机制,通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父类加载器无法完成加载任务的时候,才自己去加载。

13.为什么要采取这样的加载方式呢?

①类加载器代码本身也是java类,因此类加载器本身也是要被加载的,因此显然必须有第一个类加载器不是java类,这就是bootStrap,是使用c++写的,其他则是java了。
②虽说bootStrap extclassloader appclassloader 三个是父子类加载器的关系,但是并没有使用继承,二十使用了组合关系。
③具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,可以比较笼统的说像jdk自带的几个jar包肯定是位于最顶层的,再就是我们引用的包,最后是我们自己写的,保证了java程序的稳定性。

14.程序计数器

也叫PC寄存器,是一块较小的内存空间,他可以看做事当前线程所执行的字节码的行指示器,在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
①区别于计算机硬件的pc寄存器,两者略有不同, 计算机用pc寄存器来存放“伪指令”或地址,虚拟机的pc寄存器的功能也是存放伪指令,准确的来说,就是存放即将执行指令的地址。
②当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined+
③程序计数器是线程私有的,它的生命周期和线程相同,每个线程都有一个
④此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的情况区域。

15.Java虚拟机栈

①线程私有的,它的生命周期与线程相同,每个线程都有一个。
②每个线程创建的同时会创建一个JVM栈,JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量和引用(reference)(32位以内的数据类型,具体根据JVM位数(64或者32)有关,因为一个solt(槽)占用32位的内存空间,)、部分的返回结构,非基本类型的对象在JVM栈上仅存放一个指向堆上的指针。
③每一个方法从被调用直至执行完毕的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
⑤栈运行原理:
栈中的数据都是以战阵的格式存在,战阵是一个内存快区,是一个数据集,是一个有关方法和运行期数据的数据集,一个方法调用就产生一个栈帧,一个方法结束就弹出一个栈帧。
⑥java虚拟机栈的最小单位可以理解为一个栈帧,一个方法对应一个栈帧,一个栈帧可以执行多种指令。

本地方法栈

是线程私有的,他的生命周期与线程相同每个线程都有一个

什么是本地方法栈?

jvm中的本地方法是指方法的修饰符是带有native的但是方法体不是用java代码写的一类方法,这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。

与java虚拟机栈的对比

作用相同,区别是:虚拟机栈位虚拟机执行的java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

Java堆

是java虚拟机所管理的内存中最大的一块。
不同于上卖弄三个,堆是jvm所有线程共享的。
在虚拟机启动的时候创建。
唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
java堆是垃圾回收器的主要管理区域,因此很多时候java堆也被称为GC堆。收集器基本都采用分代收集算法。
java堆是计算机物理存储上不连续的、逻辑上连续的,也是大小可调节的。
如果堆中没有内存完成实例的分配,并且堆也无法再扩展时,会抛出OutOfMemoryError异常。

方法区

1.在虚拟机启动的时候创建。
2.所有jvm线程共享
3.除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾回收机制。
4.被用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据。
5.被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。
6.运行时,常量池是方法区的一部分。Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
7.补充
指令集时一个非常重要的概念,程序员写的代码在jvm虚拟机中时被转成了一条条指令集执行的。

类加载器子系统

1.根据给定的权限来定名 类名,来装载java文件的内容到Runtimedataarea中的方法区域。java程序员可以通过extends java.lang.ClassLoader类来自定义classloader。
2.加载过程
当一个classloader启动的时候,classloader的生存地点在jvm的堆中,然后它区主机硬盘上区装载class字节码文件到jvm的方法区,方法区的这个文件会创建一个对象在堆中,因此 这个 字节码文件有两个引用,一个指向对象,一个指向classloader。

执行引擎

1.负责执行来自类加载器子系统中被加载类在方法区包含的指令集,通俗来讲就是类加载器子系统把代码逻辑都以指令的形式加载到了方法区,执行引擎就负责执行这些指令就可以了。
2.解释器
一条一条的读取,解释并且执行字节码指令。因为它一条一条解释和执行指令,所以它很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种语言基本来说就是解释执行的。
3.即时编译器
即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码也可以执行的很快,因为本地代码时保存再缓存的。

本地方法详解

1.本地方法就是带有native标识符修饰的方法
2.native修饰符修饰的方法并不提供方法体,但是因为器实现体是由非java代码在外部实现的,因此不能与abstract连用
3.存在的意义:不方便用java语言写的代码,使用更为专业的语言写更合适,甚至有些jvm是用c写的,所以只能使用c来写。
4.更多的本地方法最好是与jdk的执行引擎的解释器语言一致。
5.各大主流操作系统的核心代码大部分是c或者c++,底层接口用汇编编写。
6.为什么native方法修饰的方法PC程序计数器为undefined。
因为类加载时,native修饰的方法被保存在了本地方法栈中,当需要调用native方法时,调用的是一个指向本地方法栈中某方法的地址,然后执行方法直接与操作系统交互,返回运行结果。郑国过程并没有经过执行引擎的解释器把字节码解释成操作系统语言,PC计数器也就没起到作用。

java内存模型

发生线程不安全问题的原因,其实在于cpu
在cpu中由一组cpu寄存器,也就是cpu的存储器,cpu操作寄存器的速度要比操作计算机主存快很多,在贮存和cpu寄存及之间存在cpu缓存,cpu操作cpu缓存的速度在主存和cpu寄存器之间,某些cpu可能由多个缓存层,计算机主存称为RAM,所有的cpu都能访问到主存,而且主存比上面的缓存和寄存器大很多。
当一个cpu需要访问到主存时,会先读取一部分主存数据到cpu缓存,进而读取cpu缓存到寄存器,当cpu需要些数据到主存时,同样会先flush寄存器到cpu缓存,然后再在某些接待你把缓存数据flush到主存。

java内存模型和硬件架构之间的桥接

正如上面讲到的,java内存模型和硬件内存架构并不一致,硬件内存架构中并没有区分栈和堆,从硬件上来看,不管是栈还是堆,大部分数据都会存到主存中,当然一部分栈和堆的数据也有可能会存到cpu寄存器。java内存模型和计算机硬件内存架构是一个交叉关系。
当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中有两个最主要的问题。

共享对象对各个线程的可见性

当多个线程同时操作一个共享对象时,如果没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能导致其他线程不可见。
例如,我们的共享对象存储在主存,一个cpu的线程读取主存数据到cpu缓存,然后对共享对象做了更改,但cpu缓存中的更改后对象还没有flush到主存,此时线程对共享对象的更改对其他CPU中的线程是不可见的。最终就是每个线程最终都会拷贝共享对象,而且拷贝的对象位于不同的CPU缓存中。
这里我们可以使用volatile解决,可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU的内存屏障指令实现的。

竞争现象

如果多个线程共享一个对象,如果他们同时修改了这个共享对象,这就产生了竞争现象。
例如:线程A和线程B共享一个对象obj,假设线程A从主存读取的obj.count变量到自己的CPU缓存,同时,B也进行了相同操作,他们都对count进行了+1,此时,count就被执行了两次,在不同的CPU缓存中。
如果这两个加一操作是串行操作,那么count会在原始值上加2,如果是并行,那么无论谁先flush到主存,最终都是只加了一次1。
要解决上面的操作,我们需要使用synchronized代码块。
synchronized代码块,可以保证同一个时刻只有一个线程进入代码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。

volatile和synchronized区别

volatile本质是告诉jvm当前变量在寄存器中的值是不确认的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
volatile仅能使用在变量级别,synchronized则可以使用在变量、方法、和类级别。
volatile仅能使用变量的修改可见性,不能改变原子性,而synchornized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

方法区和堆的区别

方法区存放了类的信息,有类的静态变量,final类型的变量,field自动信息、方法信息,处理逻辑的指令集,我们仔细想想一个类里面也就这些东西,而堆中存放的是对象和数组,咋一看好像方法区跟堆的作用是一样的。其实呢,这里就关系到我们平时说的对象是类的实例,是不是有点恍然大悟了?
这里的对应关系是方法区->类,堆->对象,方法区的类是唯一的,同步的。

方法区的内容是一次把一个工程的所有类信息都加载进去再去执行还是边加载边执行呢?

其实单从性能方面也能够猜测到时只加载当前使用的类的,也就是边加载边执行,例如我们使用tomcat启动一个spring工程,通常启动过程中会加载数据库信息,配置文件中的拦截器信息,service的注解信息,一些验证信息等等,其中的类信息就会率先加载到方法区,但如果我们想让程序启动的快一点,就会设置懒加载,把一些验证去掉,如一些类信息的加载等真正使用的时候再去加载,这样说明了方法区的内容可以先加载进去,也可以使用到的时候加载。

方法区,栈,堆之间的过程

类加载器加载的类信息放到方法区,执行程序后,方法区的方法压入栈的栈顶,栈执行压入栈顶的方法,如果需要开辟堆空间则开辟。

猜你喜欢

转载自blog.csdn.net/gggggym/article/details/121819296