java-虚拟机(上)
文章目录
JVM组成
按照大的来分可以分为三部分:
- 类加载器,由于java是纯面向对象语言,类加载器会把java类转换成成字节码
- 运行时数据区(内存分区):再细分可以分为共享的方法区和堆,线程不共享的虚拟机栈和本地方法栈,还有每个线程的指针
- 执行引擎:将中间代码转换成机器指令(x86,arm等)
- 本地库接口
运行时数据区详细介绍
- 堆(线程共享)
- 方法区(线程共享)
- 程序计数器(线程独占)
- 虚拟机栈(线程独占)
- 本地方法栈(线程独占)
程序计数器
就如同cpu中的pc,虚拟机中也有对应代码的pc,为了实现并发,每一个线程程序运行到哪里都不一样,也就需要保存,恢复上下文也方便。所以在jvm中程序计数器是私有的。
注意:pc是唯一一个不会发生OOM的内存区域,估计是因为本来就放一个指针,再怎么样也超不出去。生存周期,随着线程的创建而创建,随着线程死亡而死亡。
虚拟机栈
这里的虚拟机栈跟真实的用c语言编译的栈类似,都是存函数调用的,有返回地址,局部变量,操作数,这里还有一个动态链接
- 局部变量表:主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
- 操作数栈:主要是作为方法调用的中间站,比如addi a,b,a(估计不是这么写的,反正就是a+b计算出来的值再赋给a),存放的那个临时变量就a+b就放在操作数栈
- 动态链接:运行到一定位置的时候可能会需要调用其他的类或者方法,这个时候就要把符号引用转换为调用方法的直接引用,因为再编译的时候都是用的常量池,在常量池引用的,一层套一层,这个时候就要把最核心的那个函数给拿出来,变成直接引用
虚拟机栈超出会报错的,栈帧数量不能多于一个值。比如无限递归,最后报错报的是栈溢出,而不是堆溢出,因为在爆堆之前就已经爆栈
本地方法栈
这个跟虚拟机栈很像,但是虚拟机栈是为了java语句服务的,本地方法栈是使用到的本地native方法服务
堆
注意,我们在这里说的这五个其实是逻辑部分,就好像计算机组成原理中那五个部分一样,但是实际cpu又是控制器和运算器组合的。
这里也是类似,可以理解为这是jvm的逻辑设计图,具体实现的比如jdk1.7的持久代和1.8的元空间,其实也只是方法区的一个实现罢了
持久代和元空间
最大的一块,jdk1.7主要是三块,新生代,老年代和持久代,1.8把持久代取消了,取而代之的是元空间
个人理解,本来在堆中就完成了方法区的设计,在持久区放静态变量,代码块和编译好的代码,但是由于可能会出现oom,以及越来越多的动态类,时的很容易爆堆,这个时候不如把它提出虚拟机吧!放在实际内存下面,这样就有更广阔的空间给你爆了。
于是元空间这个概念就出来了,但是也不是无限扩大,有大小限制的
为什么使用元空间?
- 1)由于 PermGen 内存经常会溢出,引发OutOfMemoryError,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM。
- 2)移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
- 3)减轻gc的负担,放在外面的元空间可以不用gc
准确来说,Perm 区中的字符串常量池被移到了堆内存中是在 Java7 之后,Java 8 时,PermGen 被元空间代替,其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如 java/lang/Object 类元信息、静态属性 System.out、整型常量等。
元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
新生代和老年代
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
- 老年代主要保存生命周期长的对象,一般是一些老的对象
这就涉及到后面的垃圾回收算法了
方法区
一般来存静态变量,常量以及编译好的代码
这里要注意,既然方法区里面有常量,那也会有运行时常量池。这里要区分字符串常量池,jdk1.7之前,字符串常量池是跟持久代放在一起的,是持久代的一个组成部分,1.7之后,字符串常量池就放在堆空间里了,也就是说提出来了,直到现在也还是在堆空间
为什么?
主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
而运行时常量池是放在方法区的,也就是元空间
类加载器与双亲委派模型
这里的加载有点像dns的递归查找
- bootstrapClassLoader:根加载器,每个都会从它开始
- ExtClassLoader:扩展功能的一些jar包里面的类
- AppClassLoader:应用类加载器
- 自定义,可以重写方法
双亲委派模型
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类
好处:
- 防止核心库被篡改
- 不重复加载类
打破双亲委派
tomcat,重写了loadClass方法