0


浅谈Java虚拟机(JVM)

前言👇

    第一次在CSDN上写博客(2022.03.24,大二下),历时×××天,期间因为准备蓝桥杯有所延误。思考了一下,决定在第一篇写一写JVM,不能保证所有的东西都是对的,虚心求教,有不对的地方请大佬们多多指教(doge)。

正文👇

★JVM运行流程:

如图:

此图描述了JVM虚拟机运行时的基本流程:①将.java等文件进行“前端编译”之后,获得相应的字节码文件(.class)。②使用相应的类加载器将类加载进虚拟机中。③再根据程序员所编写程序的业务逻辑在运行时数据区中进行操作。④执行引擎将字节码指令解释、编译为相应平台上的本地机器码指令。⑤CPU识别机器码指令进行操作。

★★★类的加载:

类的加载分为加载-->链接-->初始化。

加载:

    在内存中生成这个类的一个java.lang.Class的对象,作为方法区中这个类的数据访问的入口。

链接:

** 可以把链接进一步分为验证-->准备-->解析。**在验证阶段,虚拟机会对字节码文件的格式、语义、字节码、符号引用进行验证。判断字节码文件是否符合要求和规范(cafa babe),判断字节码是否可以被虚拟机正确地执行和判断在系统中是否存在这个类或方法的符号引用,真正的将符号引用转化成直接引用是发生在解析阶段的(或者在运行时进行动态链接)。在准备阶段,主要执行的是类变量的分配内存、默认初始化(赋默认值)和static&final常量的显式初始化(具体可见初始化↓)。在解析阶段,最主要的是将类、方法和接口的符号引用转化为直接引用。

符号引用:在类加载的时候,可能并不知道所引用的类的地址,因此可以使用一些特定的字面量来表示所引用的类。

直接引用:类、类变量、类方法直接指向方法区。此阶段属于静态链接,是相对于栈帧中的动态链接而言的,两者执行的时期不同,前者发生在类的加载阶段,后者发生在程序执行时期。

初始化:

** 初始化阶段主要进行类变量的显式初始化、执行静态代码块、执行<clinit>。**在一个类中存在成员变量,成员变量分为类变量和实例变量,变量可以是基本数据类型(int、char...)或者是引用数据类型(类、数组、接口)。成员变量都放在堆中,基本数据类型会指向方法区的运行时常量池中的字面量,引用数据类型指向堆中的对象。用final关键字修饰的变量叫做常量,常量在编译时期就初始化了,存放在字节码文件的常量池中。

    //类变量
    static int a=10;//准备(赋默认值0)-->初始化(10)
    static Integer b=10;//准备阶段(赋默认值null)-->初始化(10)

    static final int c=10;//编译阶段初始化
    static final Integer d=10;//编译阶段初始化

    //实例变量
    int a1=10;//创建对象时初始化
    Integer b1=10;//创建对象时初始化

    final int c1=10;//编译阶段初始化
    final Integer d1=10;//编译阶段初始化

    /*创建对象的六个步骤:
        1.判断这个所属的类是否被加载过
        2.为对象分配内存空间
        3.处理并发安全问题
        4.初始化所分配到的空间
        5.设置对象头
        6.*执行<init>,真正创建对象 
    */

类加载器:

   1.引导类加载器(Bootstrap ClassLoader)

   2.扩展类加载器(Extension ClassLoder)

   3.系统类加载器(System ClassLoder)

   4.自定义类记载器

   他们之间从上到下属于上下级的关系,并不属于继承关系,Bootstrap ClassLoader是最顶层的类加载器,它可以用于加载Java_HOME lib下的jar包和类文件。扩展类加载器可用于加载JAVA_HOME lib/ext 下的jar包和类文件。System ClassLoder在导平常用的jar包和自己创建的类的时候用到,并且自定义类加载器必须继承了这个System ClassLoder之后才能生效。

    能够通过对象.getClassLoader()方法获取到当前对象所属类的类加载器,getParent()方法获取上一级的类加载器。

了解了类加载器的分类之后,我们便可以引出“双亲委派机制”👇

双亲委派机制:

    **双亲委派机制**是指:受到一个类的加载请求之后,类加载器并不会直接开始加载,而是将这个请求委托给他的上级加载器,一直委托给Bootstrap ClassLoader去加载,如果发现不属于这个加载器的加载范围,则会将这个请求再往下进行委派,一直到找到所属的类加载器为止。

优点:1.避免类被重复加载。 2.保护核心API不被篡改、保护核心类库。(沙箱安全机制)

★★★运行时数据区:

** **

如上图是来源于阿里的运行时数据区的内存分配图👆

由此可知运行时数据区包括了堆区、元空间、虚拟机栈、程序计数器、本地方法栈。

程序计数器:

    程序计数器用于记录**下一条字节码的指令地址,**它是线程私有的,程序计数器中**不存在GC**(垃圾回收)也**不存在OOM**(OutOfMomery 内存溢出),每一个线程都会分配一个PC寄存器,如果被作为线程公用,则会出现错乱,比如线程1下一条执行第5行的代码,线程2下一条执行第10行的代码,当线程1的CPU时间片用完轮到线程2后,线程2执行到第15行代码时间片又轮到线程1了,那么线程1下一条就会执行第15行的代码,这明显和我们的需求不一致。所以这样就会出现错乱!

⭐虚拟机栈:

    虚拟机栈是内存中非常重要的部分,堆管存储、栈管运行。

    虚拟机栈中又很多栈帧,一个栈帧对应一个方法,一个栈桢中包含**局部变量表**、**操作数栈**、动态链接、方法返回地址和一些附加信息。局部变量指的是存在于一个方法内部的变量,所以顾名思义,局部变量表是存放局部变量的集合,它是一个**数字数组**,32位占一个槽(slot)。根据我的理解,因为在栈中是不可能存放对象的,这里涉及到**逃逸分析**,如果一个局部变量实在方法内被创建的并且没有被返回或者被其他方法所涉及,就称它没有逃逸,因此可以进行栈上分配并且进行标量替换,所谓的标量,我泛泛地理解为基本数据类型,说到这里就可以得知为什么是一个数字数组,因为基本数据类型都可以理解转化为数字的类型(boolean-->0/非0,char-->Ascii码...)。

    操作数栈可以存放计算的中间结果,变量临时的存储空间。他在程序运行的时候将方法内的每一条字节码指令入栈在需要的时候出栈,完成计算。

    动态链接指将符号引用转化为调用方法的直接引用,不同于在解析阶段的“静态链接”,动态链接发生在程序运行时,指向方法区的运行时常量池中。

    方法返回地址个人理解为该栈帧(方法)运行结束之后应该将结果返回的一个地址。

本地方法栈:

    在本地方法栈中存放的大多是一些C/C++编写的方法,与虚拟机栈类似,该区域也是有OOM无GC的。

大致介绍完了线程私有的内存区域,下面就是内存共享的区域了,在内存中也是非常重要的!

⭐堆区:

    **在堆中存放的是对象。**堆中主要分为新生代、老年代,其实可以把字符串常量池单独提出来讲。新生代与老年代的大小比例为1:2,在新生代中还分为**伊甸园区**(Eden)、**S1区**(幸存者一区)、**S2区**(幸存者2区),他们之间的比例为8:1:1。伊甸园区中的对象大部分是朝生夕死的,因为在新生代中垃圾回收(Miner GC)比较频繁,一些对象可能会被GC回收,一些可能经过GC后会被放入到幸存者区中。伊甸园区中还会有线程私有的区域:TLAB,线程的私有缓存区,可以用于解决线程的并发问题并且提高内存吞吐量。

如果对象被GC后放在了幸存者区,每一次Miner GC都会将对象由原先的S区(From区)转移到另一个S(To区)区,并且分代年龄加一。上文(代码块中)提起过对象创建的步骤,在其中一个步骤中会给对象设置对象头,在对象头中存放了对象的GC分代年龄,一个对象的分代年龄如果达到了15次那就会被放入到老年代中,需要注意的是:幸存者区的GC并不是主动进行的,而是伊甸园区进行一次GC之后他才会进行回收(被动)。

    **老年代**中的对象基本上是比较稳定的,使用比较频繁的,因此老年代中的Major GC不是很多次。

    问:**为什么要分代?**答:**提高提高GC的性能,加快回收的效率,将使用率低和使用率高的对象分开。频繁回收新生代,较少回收老年代。**

 **  对象存放的流程,重点!!!👇**

** 图来源于 尚硅谷**

①对象是否能被放入伊甸园区?直接放:(Minor GC之后能不能放?直接放:去老年代);

** ②Minor GC之后伊甸园区中的一部分对象是否能放入到幸存者区?直接放:去老年代;**

** ③进入老年代中后并不是直接可以放。对象是否能放入老年代?直接放:(Major GC后能不能直接放?直接放:报OOM);**

⭐元空间:

    元空间和永久代一样都是方法区的具体实现,永久代在jdk1.8被替换为元空间。在1.6字符串常量池在永久代中,1.7被移到了堆区中。

    在元空间中主要存放的是**类型信息、运行时常量池、JIT缓存代码、域信息、方法信息**。元空间中存在OOM和GC,需要注意的是元空间是利用直接内存的。

    如果在在堆中创建了一个类的实例对象,这个对象的对象头中的类型指针会指向方法区的类元信息(Klass 可见上上图)。在运行时常量池中包含了字面量和符号引用,因此一些基本数据类型的变量会指向运行时常量池,在栈帧中的动态链接也会指向运行时常量池。JIT(即时编译器)也会将一些编译后的本地代码放在方法区中,域信息(Filed)是一种属性,可以是类变量也可以是实例变量,在这里我们不做深究,同时在方法区中一定会包含一些方法信息,这是毋庸置疑的。

深究“对象”:

     **创建对象**的六个步骤:
              1.判断这个所属的类是否被加载过
              2.为对象分配内存空间
              3.处理并发安全问题
              4.初始化所分配到的空间
              5.设置对象头
              6.*执行<init>,真正创建对象 

      对象的创建不是一个简单的步骤就足够完成的,即使前5个步骤并不是“真正”的创建对象,但是也非常重要。在最后一个步骤中虚拟机为实例变量显式初始化、执行代码块并且执行<init>方法之后,对象才能算真正的创建了。

      对象也并不是只存放它所表示的实例信息(实例数据)这么简单,他还包含了对象头,对象头中由运行时元数据和类型指针,同时对象中还有“对齐填充”。这也是面试的热点,在对象头中的运行时元数据里包含了很多东西,对象的哈希值、GC分代年龄、锁的状态、线程持有的锁、偏向线程ID、偏向时间戳。这都是构成对象头很重要的部分。

      **关于String:**

** **对于String对象的创建有两个方法:①String str=new String(“abc”);②String str=“abc”;

            追根溯源他们两个是完全不一样的,第一种方法是通过new关键字来创建实例,这种方法会在堆中创建一个String对象的同时在字符串常量池中也添加一个“abc”字面量。而第二种方法则直接让str变量指向字符串常量池中的字面量“abc”,如果常量池中不存在的话会在常量池中创建一个“abc”。

          **  进阶:**常量之间拼接不会在堆中创建对象,变量拼接可能会影响到堆和字符串常量池。
        //常量拼接
        String str="a"+"bc";
        final String t1="a";
        final String t2="bc";
        String str1=t1+t2;
        //变量拼接
        String str2=new String("a")+new String("bc");//1.a 2.bc  3.a 4.bc 5.StringBuilder 6.abc
        String str3=new String("a")+"bc";//1.a 2.bc   3.StringBuilder 4.a 5.abc

intern()方法:

    intern()方法是将指针指向字符串常量池中指定常量的方法,如果常量池中不存在,则先创建一个再指向。
        String s1_0="bcd";
        String s1_1=new String("xyz");
        String s1_2=new String("abc")+new String("xyz");
        
        s1_0.intern();//Ⅰ
        s1_1.intern();//Ⅱ
        s1_2.intern();//Ⅲ

Ⅰ:因为已经在常量池中创建了一个“abc”字面量了所以intern()方法会将指针指向字面量“abc”。

Ⅱ:在new的同时也在常量池中创建了“xyz”字面量所以也指向“xyz”。

Ⅲ:在常量池中没有“abcxyz”(不懂的见上文👆)所以要先创建一个字面量然后再指向它。

小结:

        此文简单介绍了一下JVM的内存空间划分、运行流程等相关的知识,之后打算说一下垃圾回收和JVM的性能优化等,有不对的地方还请大家多指教指教,小白在此。谢谢大家的支持!
标签: java

本文转载自: https://blog.csdn.net/qq_62952874/article/details/123708085
版权归原作者 靓仔炜 所有, 如有侵权,请联系我们删除。

“浅谈Java虚拟机(JVM)”的评论:

还没有评论