通俗易懂理解JAVA虚拟机

2年前 (2022) 程序员胖胖胖虎阿
156 0 0

目录

前言    

一、什么是JAVA虚拟机(JVM)

二、内存结构

1.程序计数器

2.虚拟机栈

3.本地方法栈

4.堆

5.方法区(元数据区)

6、执行引擎

7、直接内存

三、垃圾回收

1.如何判断对象可以回收,

2.垃圾回收算法

3.分代垃圾回收

4.垃圾回收器

四、垃圾回收调优

五、类加载器子系统

1.类加载过程:

2.类加载器以及之间的关系

3.双亲委派机制

4.沙箱安全机制

六、Class文件格式(ClassFileFormat)

1.字节码文件的跨平台性

2.虚拟机基石

总结


前言    

       本文以HotSpot虚拟机为代表重点是介绍一下java虚拟机内存结构,垃圾回收机制,类加载子系统。


一、什么是JAVA虚拟机(JVM)

        虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
简单来说JVM是用来解析和运行Java程序的。

二、内存结构

通俗易懂理解JAVA虚拟机

通俗易懂理解JAVA虚拟机

JVM的内部体系结构分为三部分,分别是:类装载器(ClassLoader)子系统,运行时数据区,和执行引擎

java虚拟机定义了若干的程序运行期间会使用到的运行时数据区,其中会有一些随着虚拟机启动而启动,随着虚拟机的退出而销毁,比如线程独享的存储区域。

线程共享:堆,堆外内存(元空间)
线程独享: 计数器,本地方法栈,虚拟机栈。

1.程序计数器

pc程序计数器:存储指向下一条指令的地址,由执行引擎读取下一条指令,线程私有,运行速度最快的存储区域,他是唯一一个没有一个内存溢出(out of memery)的区域

如果线程正在执行的是java虚拟机栈的方法时,程序计数器记录的是java虚拟机 正在执行字节码指令地址,如果执行的native方法,则记录的是Underfined。

2.虚拟机栈

虚拟机栈: 线程运行需要的内存空间,每个方法在执行时都会创建一个栈帧,栈帧:每个方法运行时需要的内存,一个栈内有多个栈帧。栈帧存储局部变量表,动态链接,操作数,方法出口等信息。一个方法从调用到结束就是一个栈帧从入栈到出栈的过程。

局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型,就是一些操作完以后的数据,它是一个数组结构。(returnAddress中保存的是return后要执行的字节码的指令地址。)
操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去,临时来存放数据。
动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。

出口:出口正常的话就是return 不正常的话就是抛出异常落

1.垃圾回收是否涉及栈内存?

  答: 不需要,这里是一定不能有垃圾的,如果这里有垃圾那么栈帧会弹不出去,整个虚拟机栈会卡死。
2.栈内存分配越大越好吗? 

答: 不是,因为栈内存一定的情况下,分配越大,栈帧的数量就会减少
栈内存溢出StackOverFloatError: 方法的递归调用,栈帧过多,栈帧过大,第三方的库都可能会导致,  调优 -Xss  

3.本地方法栈

java虚拟机调用本地方法时分配的内存,本地方法就是一些不是由java编写的代码比如一些由c/c++编写的方法,本地方法栈就是给本地方法运行提供内存空间。

4.堆

线程共享,这块是java虚拟机内存结构中最大的一块区域,也是垃圾收集的主要区域,线程共享,主要存放java创建的实例和数组,当堆中没有内存存放实例时会oom,内存溢出。

堆内存诊断

jps:查看当前系统中有哪些java进程,命令行形式

jmap:查看堆内存的占用情况(某个时刻),命令行形式  jamp -heap 进程id

jconsole:多功能的检测工具,可以连续监测,图形界面

jvirsualvm:图形化界面工具,可以排查堆内存占用情况,堆dump。

相关VM参数

 堆初始大小 : -Xms

堆最大大小:-Xmx

新生代大小: -Xmn

幸存区比例: -XX: SurvivorRatio=

GC详情:-XX:+PrintGCDetails -verbose:gc

FullGC前MinorGC: -XX:+ScavengeBeforeFullGC
晋升阈值:-XX:MaxTenuringThreadhold=threashold

5.方法区(元数据区)

线程共享,方法区逻辑上是堆的一部分,方法存储了更类结构相关的一些信息,比如常量,类变量,类的构造器,方法的信息,成员方法和构造方法,编译器编译后的代码等等,方法区如果内存不足也会报内存溢出

在jdk1.8以前,称为永久代,在1.8以后,称为元空间MateSpace,不由具名管理它的内存结构,而是交给操作系统内存,元空间使用的系统内存,元空间的串池StringTable被移到了堆内存中。
代理技术在运行期间动态生成字节码,可能会出现内存溢出,场景: Spring ,mybatis。
常量池:存放编译期间生成字面量和符号的引用 ,虚拟机根据这张常量表找到要执行的方法名,类名,参数类型,字面量等信息。
运行时常量池: 当常量池是在*.class 文件中的,当类被加载,它的常量池信息就会放入运行时常量池中,并把里面的符号地址编号才能真实地址。
常量池(contanst pool)和串池(String Table)的关系: 


public class a {
    public static void main(String[] args){
        String s1="a";
        String s2="b";   //懒惰的
        String s3="ab";
        String s4 =s1+s2;  //new StringBuilder().append().toString(),StringBuilder的 
                           toString是new了一个新对象,相当于new String("ab");
        String s5="a"+"b"; //javac在编译期间的优化,在编译就已经确定为ab
        System.out.println( s3 == s4); //false  s3是在串池中的,而s4是在堆里面的,地址不同
        System.out.println(s3 == s5);  //true  这里跟上面不同之处在于 "a"和"b"是常量,
                    而s1和s2是变量,常量在编译器就已经确定,不需要用StringBuilder去拼接
    }

}

串池(StringTable)的特性:

常量池中的字符串仅仅是符号而已,第一次用到才变为对象;

利用StringTable的机制来避免重复创建字符串对象;

字符串变量拼接的原理是StringBuilder创建新对象;

字符串常量拼接原理是编译器优化;

可以使用intern方法,主动将串池中还没有字符串对象放入串池;

StringTable也会触发垃圾回收。

 jdk1.8这个字符串对象尝试放入串池,如果有并不会放入,如果没有则放入串池,会把串池中的对象返回。

jdk1.6将这个字符串对象尝试放入串池,如果有并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回。

public class a {
    //串池的字符["a","b","ab"]
    public static void main(String[] args){
        String s = "ab";
        String s1= new String("a") + new String("b"); //new String("ab");
        String s2 = s.intern(); //将这个字符串对象尝试放入串池中,如果有则不放入,没有就会把串池的对象返回

        System.out.println(s2 == s1);  //false
        System.out.println(s2 == "ab");//true

    }

}

串池的位置:

在jdk1.6串池StringTable是常量池中的一部分,随着常量池放在永久代(方法区)中

在jdk1.8串池StringTable是放在堆中,

永久代的内存回收效率很低,永久代得到fullGC时才会进行垃圾回收,而一个java对象中大量的字符串都放在StringTable中,StringTable用的非常频繁,如果放在堆中的话,只需要minGC既可以触发垃圾回收

6、执行引擎

从代码的执行角度来讲,执行引擎是非常重要的,是JVM核心结构之一。

JVM的主要任务是负责 装载字节码到内部,但是字节码并不能直接运行在操作系统之上,以因为字节码的指令并非等价于本地机器指令,它的内部包含的仅仅只是一些能够被JVM所识别的字节码指令,符号表。那么,想让一个Java程序运行起来,还需要执行引擎安静会字节码指令解释、编译成对应平台上的本地机器指令才可以。简单理解就是,一个翻译官。

 java采用混合模式:混合使用解释器 + 热点代码编译,java的半解释半编译并不是先解释再编译的而是它可以解释也可以编译。

通俗易懂理解JAVA虚拟机

通俗易懂理解JAVA虚拟机

1.解释器(图中绿色部分)

将源代码翻译成字节码文件,然后在运行时通过解释器假尼姑字节码文件翻译成机器指令,时至今日,Java虚拟机其实不只是面对Java这门语言,因为任何class文件都能在Java虚拟机上运行。

2.及时编译器 Just In-Time intepreter(图中蓝色部分)

由于解释器在设计和实现上非常简单,除了java外还有很多语言也同样是基于解释器执行,基于解释器执行已经沦为低效的代名词,为了解决这一问题,JVM提供一种叫做及时编译器的技术。

优势:速度快,已经可以和C/C++一较高下了。

既然如此,为什么要保留解释器?

首先,解释器的优点是响应速度快,解释器可以马上发挥作用,省去编译时间,而不必等待即时编译器全部编译完成再去执行,这样可以省去很多不必要的编译时间,并且随之程序运行时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。

热点代码检测:

多次被调用的方法(方法计数器:监测方法执行频率)

多次调用循环(循环计数器: 监测循环执行频率)

通俗易懂理解JAVA虚拟机

7、直接内存

直接内存并不是java虚拟机的一部分,不受JVM内存回收管理,但是会内存溢出

常用于NIO操作时,用在数据缓冲区

分配回收成本较高,但是读写性能高

java本身并不具备 磁盘 读写的能力,java要调用磁盘文件的内容需要调用本地方法,这里会牵扯到CPU的运行状态,由用户转态转换到内核转态,其次,当CPU切换到内核状态时内存这边也会划出一块系统缓冲区中,,而java代码是没办法直接运行的,所以会在java对内存中划分出一块java缓冲区,这时会有两块缓冲区,读取的时候必然会有不必要的复制,降低效率。

JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。java代码能直接访问这块区域,磁盘文件也能直接访问这块区域。所以就提高读写效率。

 直接内存和堆内存的区别:

直接内存的读写效率 高,分配回收成本高

堆内存的读写效率 低 , 分配回收成本低

三、垃圾回收

1.如何判断对象可以回收,

1.1引用计数:给对象一个计数器,难以解决对象之间循环引用的问题,会造成内存泄漏。

1.2可达性分析,java虚拟机中的垃圾回收器采用的是这种算法判断GC ROOT是否有相连的引用链,如果没有就回收。

1.3四种引用(强度递减)

强引用:只要沿着GCroot引用链,就不能被回收。

软引用,只要没有被强引用引用,就可能会被回收,当我垃圾回收时,内存不够就回收;软引用本身也是一个对象,当软引用对象被回收时,软引用会进入引用队列。

弱引用,只要没有被强引用引用,就可能会被回收,当我垃圾回收时,不管内存够不够都回收,同理,软引用也会进入引用队列。

虚引用,必须配合引用队列使用,当虚引用对象创建时,他会关联一个引用队列,主要配合           

终结器引用,必须配合引用队列使用 

2.垃圾回收算法

复制算法 

 特点:效率高,但是会浪费空间,复制算法主要用在新生代,因为大多数的对象都是朝生夕死的,没办法熬过第一次的GC,所以没有必要用标记算法。

垃圾清除算法 

特点:会产生空间碎片, 用在老年代,老年代中对象存活率较高、没有额外的空间分配对它进行担保

垃圾整理算法 

 这是复制算法和标记清除算法的折中,内存利用率高,而且不会产生内存碎片,用在老年代。

3.分代垃圾回收

通俗易懂理解JAVA虚拟机

在谈论垃圾回收器之前我们得先了解分代模型

根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。
新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。新生代和老年代的比例是1:2,

新生代

在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

  • 新生代的特点

    • 所有的 new 操作分配内存都是非常廉价的
      • TLAB thread-lcoal allocation buffer
    • 死亡对象回收零代价
    • 大部分对象用过即死(朝生夕死)
    • Minor GC 所用时间远小于 Full GC
  • 新生代内存越大越好么?

    • 不是
    • 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
    • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长

老年代

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记清除法或者标记整理算法进行回收。

Full GC Minor GC ,Major GC的概念

FullGC

清理的是整个堆空间包括新生代和老年代

SerialGC,ParallelGC,CMS,G1折四种垃圾收集器早新生代内存不足发生的垃圾收集都是-minor gc,Serial和ParallelGC在老年代内存不足发生的垃圾收集机制都是-full gc;

CMS和G1这两个垃圾收集在老年代有所不同,对于G1来说,新产生的垃圾跟不上回收的速度,这时候并发收集就失败了,这时候就会退化成FullGC,就会特别的慢,更长时间的Stop the world,响应速度变慢;对于CMS也一样,他们工作在并发的阶段,回收速度高于垃圾产生速度,GC日志是不会有FullGC的字样的。

MinorGC

Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。

MajorGC

Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。

4.垃圾回收器

所谓的垃圾回收,跟jdk版本有很大的关系,不同版本的垃圾回收器在内存的管理是不一样的,jvm的内存管理由垃圾回收器来决定。

随着内存变得越来越大,诞生了很多种垃圾回收器,jVM总共有十种垃圾回收器,这篇介绍的是前面的六种和G1

通俗易懂理解JAVA虚拟机

前面六种是分代模型,就是内存里面分成两代,分为新生代和老年代

G1是逻辑分代,物理不分代,

ZGC逻辑不分代,物理也不分代,

Shenando和ZGC他们很像

Epsilon,是jdk11新出的,特殊的一个,他什么事都没干

垃圾回收器的组合:

凡是图中有连线的都可以组合使用,但是常用的组合就三种

ParNew和CMS,Serial和SerialOld,  ParallelScavenge和Parallelold

查看虚拟机用的是那种GC的命令

通俗易懂理解JAVA虚拟机

六种分代垃圾回收器

通俗易懂理解JAVA虚拟机

这几个垃圾收集器一般是配合使用

1.串行:单线程,适合堆内存小,垃圾回收线程工作时其他线程都得停止(Stop The World)

Serial,工作在新生代,复制算法

SerialOld,工作在老年代,标记-整理算法

2.吞吐量优先:多线程,适合堆内存比较大,多核CPU支持,适合服务器,让单位时间内Stop The World时间变短,垃圾回

ParallelScavenge:新生代,复制算法,垃圾回收线程跟CPU核数有关

ParallelOld:老年代,标记整理算法,这两种是JDK 1.8默认的垃圾回收机制

3.响应时间优先:多线程,适合堆内存比较大,多核CPU支持,适合服务器,尽可能让单次STW时间变短

ParNew:新生带,复制算法,serial的多线程版本

CMS:Concurrent Mark Sweep,老年代,用户线程和垃圾线程并行,stop The World时间短。

分为以下几个阶段

  • 1、初始标记(CMS initial mark)会产生停顿,非常短,仅仅是扫描引用链
  • 2、并发标记(CMS concurrent mark),从root开始找,一边找业务线程一边运行
  • 3、重新标记(CMS remark),会产生停顿,当整个图谱屡清楚以后,再重新标记一边,它的STW时间其实不会很短,但是相比于前面几种回收器来讲时间会短很多。
  • 4、并发清除(CMS concurrent sweep)清除垃圾,和用户线程一起工作

通俗易懂理解JAVA虚拟机

为什么CMS不启用标记压缩? 要保证用户线程还能够继续执行,不能改变用户线程对象的位置。

优点:并发收集,低延迟 

弊端:会产生内存碎片,导致并发清除后,用户线程可用空间不足,无法分配大对象的情况下不得不触发Full GC。

CMS对CPU的资源非常敏感,在并发阶段,他虽然不会导致用户线程停止,但是会因为占用一大部分的线程导致应用程序变慢,总吞吐量会降低。

CMS收集器无法处理浮动垃圾。比如关联的对象突然变成垃圾,那么他在第一次就不会被清理掉,就是浮动垃圾,第二次CMS才会将他们删除。

另外,由于垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序有足够可用的内存可用。因此CMS收集器不能像其他收集器那样等老年代几乎完全填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始使用回收,以确保引用程序在CMS过程中依然有足够的空间支持程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次

"Councurrent Mode Failure" 失败,这时,虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

-XX:+UseConcMarkSreepGC //手动使用ParNew + CMS
-XX:CMSInitiatingOccupanyFraction //设置堆内存使用率的阈值
-XX:+UseCMSCompactAtFullCollection //执行完full gc后进行压缩整理
-XX:+CMSFullGCsBeforeCompaction //设置执行多少次不压缩的Full GC后,来一次压缩整理
-XX:ParallelCMSThreads //cms线程数

5、G1垃圾回收器:

通俗易懂理解JAVA虚拟机

从JDK9就默认使用,使用场景同时注重吞吐量和低延迟,默认的暂停目标是200ms,G1跟cms一样都是并发,在小的堆内存场景下,他们的速度暂停速度都差不多,但是在堆内存大的情况下,G1收集器在暂停时间的优势就体现出来了,应用场景可以面向服务端应用。

设计思想:它将整个堆分成若干个预先设定的小区域块(1~32MB,大小可调),每个区域内部不在进行新旧分区,而是将

超大堆内存管理的思想,会划分为多个大小相等的区域,每个区域都可以作为伊甸园,老年区

整体是 标记整理算法,两个区域之间是复制算法。

C1回收器的垃圾回收阶段

YoungConnection,主要是对Eden区的GC,对象创建式分配到伊甸园区,Eden区的内存紧张了,对象多了,就会以复制的算法到复制到幸存区,再工作一段时间,幸存区的空间不够了,Eden区中的部分数据会之间晋升到老年代空间中,当最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

MixedConnection混合收集,在新生代和老年代都来进行规模较大的收集,当整个堆大小在具名堆栈空间中占比达到45%时,G1就会启动一次混合垃圾收集周期,同时也回收后台扫描标记的老年代分区

三色标记算法(Remark)

CMS和G1在并发标记时使用的是同一个算法:三色标记法,使用白灰黑三种颜色标记对象。白色是未标记;灰色自身被标记,引用的对象未标记;黑色自身与引用对象都已标记。垃圾回收时会根据对象的颜色去回收垃圾

漏标问题

通俗易懂理解JAVA虚拟机

 两个条件发生漏标问题:

灰色对象指向的白色对象引用消失,黑色对象指向了白色对象。

解决办法:添加屏障

  1. 跟踪黑指向白的增加
    incremental update:增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS采用该方法,但是这个办法隐藏了严重的BUG,所以CMS的remark阶段,必须重头扫描一遍。
  2. 记录灰指向白的消失
    SATB snapshot at the beginning:关注引用的删除,当灰–>白消失时,要把这个 引用 推到GC的堆栈,保证白还能被GC扫描到。G1采用该方法。
-XX:+UseG1GC //jdk9之前手动指定
-XX:G1HeapRegionSize //指定region大小,根据最小的堆内存划分成2048个区域
-XX:MaxGCPauseMillis //期望达到的最大gc停顿时间,过小单次gc的对象少,过大停顿时间长
-XX:InitiatingHeapOccupancyPercent //当整个Java堆的占用率达到参数值时,开始并发标记阶段
-XX:ParallelGCThread //并行时gc线程数,会stop-the-world
-XX:ConcGCThreads //并发时gc线程数

对象分配策略

通俗易懂理解JAVA虚拟机

java某些特定小的对象是可以在java虚拟机栈进行分配,栈里面通常会有方法的调用,一个方法对应一个栈帧,如果用栈帧来管理对象会有什么好处呢? 不需要GC参与垃圾回收,栈往外面一弹,整个对象分配结束,效率非常高。既然如此,那么为什么所有的对象都不往栈上分配?

1.栈的空间通常比较小;

2.如果一个栈帧里面有个一个引用,引用了另一个对象,这时如果这个对象被弹出去了,那么这个引用就会空指针错误。

如果这个对象够大,这个大是可以自己分配的,栈上分配不下,直接分配到老年代,如果这个对象不大不小的话就会分配到伊甸园区,不过前面还有一个线程本地分配缓冲区,不过这块区域本身就在伊甸园区,这块区域也能调优。

线程本地分配缓冲区,每个线程启动时,jVM下每个Eden去中为本地区分配缓冲区,这块区域是这个线程专属的,他的目的为了使对象尽可能快的而分配出来。如果独享在一个共享空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不需要进行任何同步,这样他们的效率又变高了。

不管TLAB空间中能不能分配的对象,对象都会在Eden空间中进行分配。如果Eden空间无法容纳该对象,就这能在老年代中进行空间分配。

四、垃圾回收调优

JVM垃圾回收调优主要就是调整下面两个指标

停顿时间: 垃圾收集器的垃圾回收线程中断应用执行的时间。-XX:MaxGCPauseMillils

吞吐量:花在垃圾收集的时间和花在应用时间的占比 -XX:GCTimeRatio=<n>

内存占用: Java堆中所占内存的大小

GC调优思路:

吞吐量大:降低gc频率,延长单次暂停时间来进行gc

暂停时间短优先:缩短单次暂停时间进行gc,提高工程频率,吞吐量下降 

配置常见的四种GC

1.SerialGC  : -XX:+UseSerialGC   #old和Young区都是用Serial

2.ParallelGC: -XX:+UseParallelGC  

Young区使用Parallel scavenge回收算法

old区使用Parallel 回收算法,参数:-XX: UseParallelOldGC来控制

3.CMS: -XX:+UseConcMarkSweepGC

4.G1: -XX:+UseG1GC  #没有young/old区

五、类加载器子系统

1.类加载过程:

程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

在这里插入图片描述

 获取类的信息使他们分配到运行时区中,类加载器负责class文件的加载,至于它能否运行,由执行引擎决定,类加载子系统负责从文件或者网络中加载Class文件,Class文件打来有特定的文件标识

加载: 

加载指的是将类的class文件读入到内存,class文件本来是一个个二进制字节,把他们装到内存中去,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。

Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来自定义自己的类加载器。

类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。

类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口。

链接: 验证--->准备--->解析

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。

准备:正式为类变量(static变量)分配内存并设置类变量默认值的阶段,这些内存都将在方法区中进行分配

解析:虚拟机常量池的符号引用替换为字节引用过程

初始化:为类的静态变量赋予初始值

2.类加载器以及之间的关系

jvm中所有的class都是被类加载器加载到内存的,一个class文件被加载到内存中,内存中其实创建了两个内容,第一块内容确实是这个class文件被加载到内存中,第二块内容是生成了一个Class类的对象Class,Class类的对象指向这个 class文件内容。

在这里插入图片描述

启动类加载器,由C++实现,没有父类,加载 lib 下或被 -Xbootclasspath 路径下的类。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null,加载扩展jar包 lib/ext 或者由 java.ext.dirs 系统变量所指定的路径下的类
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader,加载classpath指定的内容
自定义类加载器,父类加载器为AppClassLoader。

3.双亲委派机制

java虚拟机对class文件采用按需加载,如果一个类加载器收到一个类加载请求,他会先委托给父类去加载,一步一步向上委托,直到启动类加载器,父类无法加载时子类才会尝试去加载
优点: 避免类的重复加载,保护程序的安全防止程序的api被加载。

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

4.沙箱安全机制

保证程序安全

保护Java原生JDK

Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?沙箱就是一个限制程序运行的环境。沙箱机制就是将Java代码限制在JVM特定的运行范围中,防止对本地系统的破坏。

沙箱主要 限制系统资源的访问,系统资源包括CPU ,内存,文件系统,网络,不同级别的沙箱最这些资源的访问限制也不一样。

所有的Java运行环境都可以指定沙箱,可以指定沙箱策略。

六、Class文件格式(ClassFileFormat)

1.字节码文件的跨平台性

1.Java语言: 跨平台语言

当java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无需再次编译,跨平台似乎已成为一门语言必选的特征

2.Java虚拟机:跨平台的语言

Java虚拟机不包括Java在内的任何语言绑定,他只和“Class”文件这种特定的二进制文件所关联,无论实用任何语言进行软件开发,只要是能将源文件编译成正确的Class文件,那么这种语言就可以在Java虚拟机上执行,可以说,统一额而强大的Class文件结构,就是Java虚拟机的即使和桥梁!

java语言每次在发布新版本时都会发布java规范和java虚拟机规范

3.如果想让Java虚拟机正确运行在JVM中,Java源码就必须编译成符合规范的字节码文件

Java的前端编译器的主要任务就是负责这个任务的

javac是一种能够将Java源码编译成字节码的前端编译器。

javac编译器编译经历四个步骤:词法解析,语法解析,语义解析,生成字节码

前端编译器并不是JVM中的一部分,在Hotspot 虚拟机中并没有强制要求只能使用javac编译器,处理javac之外,java虚拟机该有内置在Eclipse中的ECJ编译器,和javac全量式编译不同的是,ECJ使用的是增量式编译器。

前端编译器并不直接涉及到编译优化的技术,而是将具体的优化细节交给HotSpot的后盾编译器JIT编译器负责

通俗易懂理解JAVA虚拟机

2.虚拟机基石

字节码文件面是什么?

源代码经过前端编译器会生成字节码文件,字节码文件就是一种二进制的类文件,它的内容是JVM的指令,而不是像C/C++那样经过编译器直接生成机器码。

什么是字节指令?

Java虚拟机的指令是由一个字节长度,代表着某种特定操作含义的操作码,以及跟随其后的零至多个代表此操作所需参数的操作数所构成,虚拟机中许多指令并不包含操作数,是有一个操作码

如何解读字节码文件?

在idea中安装一个jclasslib插件就可以查看字节码文件

字节码文件结构

Class类的本质

任何class文件都对应着唯一的一个类或者接口的定义信息,但反过来说,Class文件实际上并不一定是一磁盘文件的形式存在,Class文件是一组以8位字节为基础的二进制流。

Class文件格式

class文件采用一种类似于c语言结构体的方式进行数据存储,这种结果中只有两种数据类型,符号和表。

无符号数属于基本的数据类型,以u1、u2、u4、u8来表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由下表所示的数据项构成。

通俗易懂理解JAVA虚拟机

 语法糖

所谓的语法糖  ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利。

1.自动拆装箱,这个功能是jdk5加入的,将基本数据类型装换成包装类型就是装箱,将包装类型转换成基本数据类型就是拆箱,这些操作都是编译器在编译期间生成字节码,里面就包含拆装箱代码。

2.默认构造

在java文件当中进行编写一个不提供构造方法,在进行编译的时候会自动加上一个默认无参构造方法。

3.泛型集合取值

泛型也是jdk5以后添加的特性,但是在java编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码 文件之后就丢失了,实际上的类型都当做 Object来处理。简单来讲,就是泛型类型在编译成字节码文件后已经不区分是Intrger,String还是其他类型,统一当成是Object,泛型信息在这个时候丢掉了。

4.可变参数

也是jdk5引入的新特性,String ...args其实就是一个String [] args

5.foreach循环

还有一些重点不展开介绍


总结

看完课程再写完博客后感觉对JVM的理解变深了很多,虽然这篇文章的深度不够,有错误多点评,后面会修正。

版权声明:程序员胖胖胖虎阿 发表于 2022年9月23日 下午2:48。
转载请注明:通俗易懂理解JAVA虚拟机 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...