JVM的体系结构

JVM的体系结构图

在这里插入图片描述

简介
  • 从图上可以看出JVM启动时按照其配置要求,申请一块内存,并根据JVM规范和实现将内存划分为几个区域。class二进制文件信息将会被放入“方法区”,对象实例将会被放入“java堆”等
类加载器的作用

     通过一个类的全限定名来获取描述此类的二进制字节流,并将此类相关信息加载到JVM的方法区,并创建一个 java.lang.Class 对象作为此类的访问接口, class 对象的引用也保存在方法区内,每一个类加载器都有独立的类名称空间。比较两个类是否相等的前提是两个类是由同一个类加载器加载的,否则两个类比不相等。

  • 类加载器分类
       从JVM角度来讲,只有两种类加载器:启动类加载器、其他的类加载器。因为前者是JVM虚拟机的一部分,后者是独立于JVM实现的 ,跟细致的分类有以下几种
  • 1.启动类加载器(Bootstrap ClassLoader)
       作用:启动类加载器是使用C++语言实现的(HotSpot),负责加载JVM虚拟机运行时所需的基本系统级别的类,如java.lang.String, java.lang.Object等等。启动类加载器(Bootstrap Classloader)会读取 {JRE_HOME}/lib 下的jar包(如 rt.jar)和配置,然后将这些系统类加载到方法区内。 由于类加载器是使用平台相关的底层C/C++语言实现的, 所以该加载器不能被Java代码访问到。但是,我们可以查询某个类是否被引导类加载器加载过
  • 2.扩展类加载器(Extension ClassLoader)
       作用:此加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 {JAVA_HOME}\lib\ext 目录下的类库, 开发者可以直接获取此加载器, 拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的
  • 3.应用程序类加载器(Application ClassLoader)
       作用:此加载器负责加载用户类路径上指定的类库,若没有指定自定义加载器,则此加载器一般是程序中默认的加载器。应用类加载器将拓展类加载器当成自己的父类加载器。
  • 4.用户自定义加载器(Customized Class Loader)
       作用:用户可以自己定义类加载器来加载类。所有的类加载器都要继承 java.lang.ClassLoader 类并重写 findClass(String name) 方法。用户自定义类加载器默认父加载器是 应用程序加载器
    在这里插入图片描述
双亲委派模型

   双亲委派模型工作过程就是一个类加载器收到类加载的请求,它首先会把这个请求委派给父类加载器去完成,层层上升,只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。

代码测试
public class Test {
   public static void main(String args[]) {
       try {
           Object obj = new Object();
           System.out.println("Test-Test-19->" + obj.getClass().getClassLoader());                         //null
           Test test=new Test();
           System.out.println("Test-Test-22->"+test.getClass().getClassLoader());                          //sun.misc.Launcher$AppClassLoader@18b4aac2
           System.out.println("Test-Test-23->"+test.getClass().getClassLoader().getParent());              //sun.misc.Launcher$ExtClassLoader@421faab1
           System.out.println("Test-Test-24->"+test.getClass().getClassLoader().getParent().getParent());  //null
           StringBuffer sb=new StringBuffer();
           System.out.println("Test-Test-22->"+sb.getClass().getClassLoader());                            //null
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}
PC寄存器
  • PC寄存器( PC register ): 每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。 每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。保存下一条将要执行的指令地址的寄存器是 PC寄存器。PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
  • 这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 如果执行的是一个Native方法,那这个计数器是空的
方法区
  • 方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 方法区是一种规范,不同的虚拟机里面的方法区的实现不一样,最典型的就是永久代(PerGem)和元空间(Metaspace),java7的时候叫做永久代,java8开始叫做元空间
堆和栈
  • 口诀: 栈管运行,堆管存储
  • jvm栈也叫做栈内存,主管Java程序的运行,是在线程创建的时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收的访问,只要线程一结束改栈就会释放,生命周期和线程的生命周期同步,是线程私有的东西。
  • 栈保存那些东西?
  • 栈帧 : 当线程执行到某个方法时就会往线程栈中压入一个帧,称为栈帧,栈帧中包含了方法的局部变量表、操作数栈、返回地址、动态连接等信息
    1. 局部变量表: 用来存储java方法中的局部变量的,在编译期间就会分配方法局部变量表的最大容量,局部变量表以变量槽为单位,每个变量槽可以存储32位及32位以下的变量,具体大小根据变量实际占用内存而定,java的基本类型中除了long和double外其他类型都是32位以下,所以每个变量占用一个变量槽即可,而对于long和double类的变量,会占用两个变量槽,除了基本类型当然还有引用类型,引用类型变量长度JVM并没有明确定义。JVM通过索引的方式来访问变量表中的变量,索引从0开始。变量槽是可以重复使用的,当变量槽所存储的变量已经不在其作用域后,该变量槽就可以被其他变量占用
    2. 操作数栈: 用于在方法运行时可以存放以及获取操作数,其所需要的最大栈深度也是在编译期间定下的,在方法刚开始运行时,操作数栈是空的,在方法执行过程中会有各种操作指令往操作数栈中压入和获取内容,也就是出栈/入栈操作,比如一个加法指令对两个数据进行相加,运行到这里前会先将两个数据压入栈中,然后将这两个操作数取出相加;在实际情况中,方法的操作数栈之间并不完全独立,往往会公用部分空间,这样在方法调用时就不需要进行参数复制了
    3. 动态连接: 前面说了常量池中会存储方法的符号引用,而每个栈帧中都会存储一个引用,用于指向常量池中该方法对应的符号引用,字节码指令中方法的调用就以方法对应的符号引用为参数来进行,在类加载阶段的解析步骤中,部分符号引用会被解析为直接引用,称为静态解析,在方法的运行过程中,另一部分符号引用会被实时的解析为直接引用,称为动态连接
    4. 方法返回地址: 方法的运行过程中,可能会正常退出,也可能会异常退出,不论是哪种退出方式,在退出后都会要保证其上层调用者可以知道方法退出的位置,以便于程序继续执行,方法的返回地址就是用于确定退出位置的。
    总的来说: Java虚拟机栈是用来存放局部变量和过程结果的地方 ,Java虚拟机栈可能发生如下异常情况:
    如果Java虚拟机栈被实现为固定大小内存,线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。
    如果Java虚拟机栈被实现为动态扩展内存大小,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常
    在这里插入图片描述
堆体系结构(Heap)

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的,类加载器读取了类文件后需要把类、方法、常变量放到堆存中,保存所有引用类型的真实信息,以便执行器执行
在这里插入图片描述

  • 新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代
  • 老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时
  • Minor GC: 清理年轻代
    • eden、SurvivorFrom复制到SurvivorTo,年龄+1
      Minor GC的过程,首先当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden和From区,对这两个区域进行垃圾回收,经过这次回收后还活着的对象直接复制到SurvivorTo区(如果这时候有对象的年龄已经达到了老年的标准,则复制到老年区),同时把这些对象的年龄+1
    • 清空Eden、SurvivorFrom
      然后,清空Eden和SurvivorFrom中对象,也随即复制后有交换,谁空谁就是To
    • SurvivorFrom 和 SurvivorTo交换
      最后,SurvivorFrom和SurvivorTo交换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From区和To区之间来回复制,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
  • Major GC: 清理老年代
  • Full GC: 清理整个堆空间,包括年轻代和永久代
  • 所有GC都会停止应用所有线程
    在这里插入图片描述
    在这里插入图片描述
IDEA中jvm参数调整记录

在这里插入图片描述

对应的参数列表
  • -XX:+PrintGC 输出GC日志
  • -XX:+PrintGCDetails 输出GC的详细日志
  • -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
  • -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
  • -Xloggc:…/logs/gc.log 日志文件的输出路径
日志结构图解在这里插入图片描述
GC四大算法
  • 引用计数算法(Reference counting)
    算法思想: 每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作
    核心思想: 为每个对象额外存储一个计数器 RC ,根据 RC 的值来判断对象是否死亡,从而判断是否执行 GC 操作
    优点:
            1.可即刻回收垃圾:当计数值为0时,会马上回收对象到free_list,提高内存使用效率
            2.最大暂停时间短:此方法只有当mutator更新指针时才会执行垃圾回收,有效减少暂停时间
            3.没必要从根指针开始查找,减少查询时间。比如在分布式系统中会再节点内用标记-清除算法,节点间的引用时则用引用计数法
    缺点:
           1.计数器的增减操作频繁
           2.计数器需要占用一定的内存
           3.实现繁琐,*ptr=obj的地方要重写成update_ptr()
           4.循环引用无法回收
  • 复制算法(Copying)
    算法思想: 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理
    优点:
            1.GC后的内存空间是连续的,不会产生内存碎片
    缺点:
            1.将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
            2.复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低

在这里插入图片描述

  • 标记—清除算法(Mark-Sweep)
    算法思想: 标记/清除算法是几种GC算法中最基础的算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。标记/清除算法的基本思想就跟它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
    标记阶段: 标记的过程其实就是遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;
    清除阶段: 清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收。
    优点:
            1.GC后的内存空间是连续的,不会产生内存碎片
    缺点:
            1.标记和清除过程效率都不高
            2.会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存
    在这里插入图片描述
  • 标记—整理(压缩)(Mark-Compact)
    算法思想: 类似与标记清除法一样,第一步先把需要回收的对象标记,不同的是第二步把活动的对象(幸存)往内存一边移动。堆内存就像一列队伍,把所有要留下的对象都往前靠,后面剩下的都是即将回收的对象(垃圾)
    优点: 避免了“标记清除法”和“复制算法”的缺点。
    缺点: 效率比较低。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值