JVM运行时数据区、类加载机制、双亲委派

JVM运行时数据区、类加载机制、双亲委派

前言

我们先讲一下Java运行时的数据区,类加载机制,双亲委派,然后再开始讲各个垃圾回收器,每个回收器使用的垃圾回收算法,回收的过程,以及JVM的性能调优

Java运行时数据区

为了方便大家理解JVM的结构,我画了这个图,方便大家理解JVM运行时的数据区

JVM运行时数据区、类加载机制、双亲委派_第1张图片

线程共享

  • 堆区

    堆区又分为了老年代和新生代,新生代和老年代的内存空间默认比例为1:2,新生代又分为了一个Eden和两个Survivor区(这两个幸存区有些地方叫From和To,还有的资料叫S0和S1)Eden和两个幸存区的比例默认为8:1:1,这个比例可以通过参数来指定-XX:+SurvivorRatio 来指定比例,但是这个比例最好让它保持原有的比例,不需要去修改它。等下我们来验证一下这个比例。堆区用来存放对象和数组的,创建对象一般来说都是会分配到Eden区,但还有其它的情况,后面我们讲到对象内存分配的时候再具体说。

    这是虚拟机自带的一些默认的一些参数,其中SurvivorRatio我们可以看到默认值就是8,从这里就能验证Eden和Survivor的比例就是8:1:1

    JVM运行时数据区、类加载机制、双亲委派_第2张图片
  • 方法区

​ 方法区也是线程共享的,它主要存放类的属性、方法、常量、静态变量等信息。在JDK1.8之前,方法区里面有个永久代(Permanent Generation),之前字符串常量池和静态变量就是放在永久代,永久代有个比较尴尬的地方就是,在程序启动之前,必须指定大小,还不会变,FGC还不会清理这一块内存。如果这一块内存满了,只能重启。在JDK1.8的时候移除了永久代,新 增了一个元空间(MetaSpace),把原本放在永久代的常量池、静态变量都移到了元空间里面,元空间不需要指定大小, 最大的就是物理内存,这一块FGC还会对它进行回收。

线程独占

  • 虚拟机栈

    虚拟机栈里面存放的是一个个的栈帧,每个方法调用都会生成一个栈帧,用来存放方法的局部变量表操作数栈动态链接方法出口信息

  • 本地方法区

    用来存放线程执行过程中可能使用到的一些本地方法

  • PC寄存器

    用来存放线程的指令集,虚拟机的运行类似于这样的循环,主要作用是记录当前线程的执行位置,多线程情况下,线程执行到一半,

    被其它线程把执行权抢走了,就需要记录当前线程的执行位置,下次拿到执行权才知道从哪里继续往下执行。

    while(not end) {
      取PC中的位置,找到对应的指令;
      执行该指令;
      PC++
    }
    

    类加载

    类加载一共分为了三个步骤,加载–>链接–>初始化

    加载

    通过类给定的路径,将字节码文件加载到内存当中(方法区),生成一个Class对象,Class指向了这一块内存,我们后期就是通过这个Class对象去访问类里面的方法和属性。

    链接

    • 验证:验证字节码文件是否符合JVM规范
    • 准备:为静态变量赋默认值(因为这个时候类没有进行初始化,只能赋默认值)
    • 解析:将类方法、属性等符号引用解析为直接引用,常量池中的各个符号引用,解析为指针,偏移量等内存地址的直接引用,这一步其实可以放到初始化后面来进行的。

    初始化

    初始化时类加载的最后一个过程,执行静态代码块为属性赋初始值。

    双亲委派

    说到类加载,就不得不提一下双亲委派机制,JVM系统提供了三种类加载器,分别为BootStrapClassLoaderExtensionClassLoaderApplicationClassLoader,我们还可以自定义类加载器

    类加载器

    • BootStrapClassLoader

      它是最顶层的类加载器,由C和C++编写的,它主要是用来加载JAVA_HOME/lib/rt.jarCharSet.jar等核心类

    • ExtensionClassLoader

      它是由Java语言编写的,主要加载jre/lib/ext/*.jar

    • ApplicationClassLoader

      也是由Java语言编写的,用来加载ClassPath指定的内容

    双亲委派指的是每个加载器都有一个父类的加载器,自定义类加载器的父类加载器是ApplicationClassLoader,ApplicationClassLoader的父类加载器是ExtensionClassLoader,ExtensionClassLoader的父类加载器是BootStrapClassLoader,这里说的父类加载器,并不是说子类是集成子父类,其实他们之间只是通过了一个属性来进行关联。

    JVM运行时数据区、类加载机制、双亲委派_第3张图片

    这里我简单画了个流程图来说明双亲委派机制,当Application加载器接收到一个类加载的任务,首先他会去自己的缓存中找,

    是不是有这个类,有的话直接返回,如果没有的话,把任务委派给自己的父类加载器也就是ExtensionClassLoader,它接收到任务

    以后也会去自己的缓存中找,没找到继续委派,直到委派给顶层的类加载器,如果还是没有找到的话,BootstrapClassLoader会尝试去加载这个类,它发现这个目录不是我加载的,会把加载任务委派给自己的下一级类加载器,直到委派给最低层的类加载器,如果

    还是没有加载成功,就会报ClassNotFoundException

    双亲委派结构图,我已经把加载序号标好了,加载步骤依次为1 --> 2 ---> 3 --> 4 --> 5这个步骤都是到各自的缓存中去找,如果还没找到就开始尝试自己加载,加载顺序为5--> 6 -->7 ,在这七个步骤中任何一个步骤都有可能加载成功。

    类加载的作用

    主要是为了安全考虑,确保类的全局唯一性,避免用户自己定义的类,覆盖掉了JDK内部的核心类,假如我们自己写一个类名为String的类,放在自定义java.lang包下,如果采用的不是双亲委派,我们也会对这个自定义String类进行加载,把原来系统的String类可能就会覆盖掉了。

    对象的内存分配

    一个对象从创建到分配的流程,先是通过类加载的三个步骤,然后为对象申请内存空间,调用构造方法进行初始化。在了解对象的内存分配过程我们需要先了解几个知识点。

    • 栈上分配

      栈其实也可以用来给对象分配内存,它可以给那些线程私有的没有被外部调用的小对象,一般会用基本类型来代替对象

    • 线程本地分配TLAB(Thread Local Allocation Buffer)

      线程本地也可以给对象分配内存空间,这一块内存其实占用的还是Eden区,默认占用Eden1%的内存空间

    • 老年代

      存放大对象,至于多大的对象是大,这个可以通过参数来配置,(-XX:PretenureSizeThreshold)

    • Eden

      也是用来存放对象的,大部分对象都是被分配到Eden区

      一个对象的分配流程如下图

    1.首先尝试进行栈上分配

    2.判断是否为大对象,如果是大对象直接放到老年代,如果不是,尝试线程本地分配,本地分配失败,就将对象放入Eden区

    3.当Eden区满了的时候,会触发YGC,存活的对象会被放到幸存区,经过多次YGC还存活的对象,就会被放到老年代

    4.其实这里还有一个动态年龄分配担保都会讲对象放到老年代 这里我们讲垃圾回收后面再说。

你可能感兴趣的:(jvm)