JVM体系结构概述

JVM体系结构概览

JVM的位置

在说JVM体系结构之前先来看看JVM的位置。

JVM是运行在操作系统之上的,与硬件没有直接的交互,但是可以调用底层的硬件,用JIN(Java本地接口调用底层硬件)

JVM体系结构

类装载器(Class Loader)

概念

负责加载class文件,将class文件字节码加载到内存中,并将这些内容转换成方法区中的运行时数据结构。类加载器只负责class文件的加载,至于他是否可以运行,由Excution Engine决定。  

解释:

Car.class是由.java文件编译得来的.class文件,存在本地磁盘。

ClassLoader:类加载器,负责加载并初始化 类文件,得到真正的Class类,即模板。

Car Class:由Car.class字节码文件,通过ClassLoader加载并且初始化得到,这个Car就是当前类的模板,这个Car Class模板就存在方法区。

car1,car2,car3:是由Car模板经过实例化而得,一个模板可以获得多个实例化对象。

拿car1举例,car1.getClass()可以得到模板Car类,Car.getClassLoader()可得到其装载器。

种类

一共有四类。

虚拟机自带的加载器:

  • 启动类加载器,也叫根加载器(BootStrap)。由C++编写,程序中自带的类,存储在 $JAVAHOME/jre/lib/rt.jar中,如object类等

  • 扩展类加载器(Extension),Java编写,在我们看到的类路径中,凡是以Javax开头的,都是拓展包,存储在 $JAVAHOME/jre/lib/ext/*.jar 中

  • 应用程序类加载器(AppClassLoader),即平时程序中自定义的类 new出来的

用户自定义的加载器:    

    Java.lang.ClassLoader的子类,用户可以定制类的加载方式,即如果你的程序有特殊的需求,你也可以自定义你的类加载器的加载方式 ,进入ClassLoader的源码,其为抽象类,因此在你定制化开发的时候,需要你定义自己的加载器类来继承ClassLoader抽象类即可,即 MyClassLoader extends ClassLoader

Java的类加载机制,永远是以 根加载器->拓展类加载器->应用程序类加载器 这样的一个顺序进行加载的。如下图:

双亲委派机制

概念:当一个类收到类加载请求后,他不会首先去加载这个类,而是把这个请求委派给父类去完成。每一个层次的类加载器都是如此,因此所有的类加载器请求都是应该传到根加载器中的,只有当其父类加载器自己无法完成这个请求的时候(在他的加载路径下没有找到所需加载的class),子类加载器才会尝试自己去加载。

举个例子:

有一个类A.java,当要使用A类时,类加载器要先去根加载器(BootStrap)中去找,如果找到就使用根加载器中的A类,不继续往下执行,但是如果找不到,则依次下放,去拓展类加载器中找,同理找到就用,找不到继续下放,再去应用程序类加载器中找,找到就用,找不到就报classNotFound Exception的异常。

采用双亲委派机制的好处就是不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同一个Object对象。

沙箱安全机制

通过双亲委派机制,类的加载永远都是从根加载器开始,依次下放,保证你所写的代码不会污染Java自带的源代码,保证了沙箱的安全。

本地接口Native Interface

    本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。

     目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。

本地方法栈Native Method Stack

它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库

程序计数器(PC寄存器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。

静态变量+常量+类信息(构造方法/接口定义)+运行时常量池 存在方法区中

但是  实例变量存在堆内存中,和方法区无关

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧就是一个内存区块,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到占中,

A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,

B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,

……

执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……

遵循“先进后出”/“后进先出”原则。

堆、栈、方法区三者的关系

HotSpot是使用指针的方式来访问对象:

Java堆中会存放访问类元数据的地址,

reference存储的就直接是对象的地址。

体系结构

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有的引用类型的真是信息,以方便执行器执行。堆内存分为三部分:

  • Young Generation Space    新生区     Young/New 

  • Tenure Generation Space    老年区     Old/Tenure

  • Permanent Space                 永久区      Perm

堆内存逻辑上分为三部分:新生+老年+永久

实际上(物理上)堆内存的结构是: (永久区的内存区域并没有和堆其他区域在一块连续的内存空间中)

 

新生代(Young Generation Space)

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)

永久代(Permanent Space )

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

永久代在各个版本中的发展:

Jdk1.6及之前: 有永久代, 常量池1.6在方法区

Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆

Jdk1.8及之后: 无永久代,常量池1.8在元空间

在永久代中可能发生的程序异常:

        如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。

在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。

元空间有注意有两个参数:

  • MetaspaceSize :初始化元空间大小,控制发生GC阈值

  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

为什么移除永久代?

移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。

有了元空间就不再会出现永久代OOM问题了!

分代概念

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。

老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

Minor GC : 清理年轻代 

Major GC : 清理老年代

Full GC : 清理整个堆空间,包括年轻代和永久代

所有GC都会停止应用所有线程。

为什么分代?

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短

为什么幸存区分为两块相等大小的空间?

主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。

 

垃圾回收及其算法:https://blog.csdn.net/wzh66888/article/details/104735469

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章