飞道的博客

还不会JVM,是准备家里蹲吗?

435人阅读  评论(0)

JVM体系结构


JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。这样一来无论是什么操作系统或平台,通过对应版本的JVM都可预编译成字节码文件(.class),使得Java语言在不同平台上运行时不需要重新编译,也不需要修改。

JVM实际上有三种:

  1. Sun:HotSpot
  2. BEA:JRockit
  3. IBM:J9VM

用的最多(99.9%),最主要的就是HotSpot,其余两种了解即可,本文是基于HotSpot的JVM介绍。

类加载器和双亲委派机制


从源代码.java文件编译成.class字节码文件后,是通过类加载器ClassLoader文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

如下所示,都是同一个Car.Class为模板创建出来的两个不同实例化对象。
通过方法getClass()可以反射对象的Class类,此部分可参考一文掌握Java注解和反射-你总该用过@Override吧?

ClassLoader内部由下而上还分为自定义加载器、系统加载器、扩展加载器和引导加载器,加载的顺序保证了安全性,也就是所谓的双亲委派机制。

双亲委派机制:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

  • 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
  • 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。

如下图:向上级委托检查是否加载,上级自顶向下优先去加载类。

public class Car {
   
    public static void main(String[] args) {
   
        Car car1=new Car();

        Class c1 = car1. getClass();
        ClassLoader classLoader = c1. getClassLoader();
        System.out.println(classLoader);//AppClassLoader
        System.out.println(classLoader.getParent()); //ExtClasslLader
        System.out.println(classLoader.getParent().getParent()); //null
        //APP=>Ext=>Boot
    }
}
/*运行结果如下
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7c30a502
null
*/

比如说你自己定义了一个java.lang.String类,但是向上委派后加载的是根加载器的系统原来定义好的包,你自己定义的重名String不会加载。

沙箱安全机制


这趴了解即可
Java安全模型的核心就是Java沙箱(sandbox) ,什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机JVM特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,包括 CPU、内存、文件系统、网络。

当前最新的1.6以后安全机制实现,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。
虚拟机中不同的受保护域(Protected Domain)对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限。

本地方法栈


native关键字说明java的作用范围达不到了,会去调用底层c语言的库。进入本地方法栈,去调用本地方法接口JNI。换言之,JNI作用是扩展了Java的使用,融合不同的编程语言为Java所用。

从Java历史来看,Java诞生于C/C++盛行之下,不得不提供C/C++的程序接口以立足,所以它在内存区域中专门开辟了一块标记区域:Native Method Stack本地方法栈,登记native方法在最终执行的时,加载本地方法库中的方法通过JNI。在企业级应用中较为少见。

程序计数器


程序计数器(Program Counter Register),也叫PC寄存器。每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码), 在执行引擎读取下一条指令, 是一个非常小的内存空间,可以看作是当前线程所执行的字节码的信号指示器。

主要两个作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理等。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能知道该线程上次运行到哪儿了。

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间计数器互不影响,独立存储,称之为线程私有的内存。

方法区


方法区(Method Area)和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做非堆,目的是与堆分离开来。

方法区也称为永久代(PermGen),方法区和永久代的关系就像Java中接口和类的关系,类实现了接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。JDK1.8后,方法区(HotSpot的永久代)被彻底移除了,取而代之的是元空间(MetaSpace),元空间使用的是直接内存。而垃圾回收在这个区域较少出现,但并不等于数据进入方法区后就永久存在了,后续会介绍。

为什么替换永久代为元空间?
其中一个原因是因为整个永久代有一个JVM本身设置固定大小上线,无法调整;而元空间使用的是直接内存,受本机可用内存限制,并且永远不会OOM(java.lang.OutOfMemorryError)。可以使用-XX:MaxMetaspaceSize修改最大元空间大小,默认值是unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize设置元空间初始大小,若未设置则会根据运行时应用程序需求动态重新调整大小。

那什么是直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且会导致OOM异常。

插播反爬信息 )博主CSDN地址:https://wzlodq.blog.csdn.net/


(Stack)同数据结构中的栈,也就是所谓的先进后出,先入栈的元素被压在栈底,后入栈的元素则压在栈顶,出栈顺序是先弹出栈顶元素,再依次向下弹出底部元素。

比如主函数调用一个test()方法,是先入栈main,然后再入栈test,只有当test出栈后,main才会出栈,main出栈即程序结束,差不多这个亚子:

所以垃圾回收什么的不会在栈这块,要是有垃圾堵住栈的话,出入栈完成不了,程序就崩了。

栈的两种异常:

  • StackOverFlowError
    若栈的内存大小不允许超过动态扩展,当栈请求深度超过了栈的最大深度时抛出该异常。
  • OutOfMemoryError
    若栈的内存大小允许超过动态扩展,而且随着线程的创建而创建,随着线程的死亡而死亡。


(Heap)是所有线程共享的一块内存区域,在虚拟机启动时创建,是JVM所管理的内存中最大的一块。此区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

特别注意JDK1.7后,JVM已经将运行时常量池从方法区移了出来,在堆中开辟了一块区域存放运行时常量池。

堆是垃圾收集器管理的主要区域,因此堆也称为GC堆(Garbage Collected Heap),从垃圾回收的角度,由于现在收集器都采用分代垃圾收集算法(具体算法后文介绍),所以堆还可以细分为新生代老年代,再细分下去可以是:Eden空间、From Survivor、To Survivor等,进一步划分的目的是为了更好的回收内存和分配内存。

大部分情况下,对象首先会在Edun区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入S0或S1,并且对象的年龄还会加1,当它的年龄增加到一定程度(默认15岁),就会晋升到老年代。对象晋升到老年代的阈值可以通过XX:MaxTenuringThreshold来设置,这是堆内存调优的一种方式!提高老年阈值门槛来回收更多数据。

创建对象过程


如下五步:

  1. 类加载检查
    当JVM遇到一条new指令后,会先去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那就先走一遍类加载过程(前文介绍的)。
  2. 分配内存
    接下来JVM将为新生对象分配内存,对象所需内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来,分配方式有指针碰撞空闲列表两种,选择那种分配方式是由堆是否规整决定的,而堆是否规整又是GC决定的。
  3. 初始化零值
    内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),保证对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 设置对象头
    初始化零值完成后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码等,这些信息存放在对象头中。
  5. 执行init方法
    从虚拟机视角看,做完上面工作后,一个新对象就产生了。但从Java程序视角来看,对象创建才刚开始,<init>方法还没有执行,所以字段都还为零。所以一般来说new指令之后会紧接着<init>方法,把对象按程序员意愿初始化。

对象访问方式


建立对象可不就是为了使用对象,Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,主要分使用句柄直接指针两种。

  1. 使用句柄
    堆中划分一块内存出来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息:
  2. 直接指针
    若直接使用指针访问,堆的布局中就要考虑如何放在访问类型数据的相关信息,而reference中存储的直接就是对象的地址。

垃圾判断


对象:
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已死亡,即不能在被任何途径使用的对象。

  1. 引用计数法
    给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器就减1,任何时候计数器为0的对象就是不可能再被使用的。
    但是给每个对象开计数,也是一种消耗。
  2. 可达性分析算法
    基本思想时通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可用的。

常量:
运行时常量池主要回收的是废弃的常量,那么如何判断一个常量是废弃常量?
设在常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量“abc”是废弃变量。

类:
判定废弃常量比较简单,但判定类是无用类就较苛刻了,需要同时满足以下3点:

  1. 该类所有实例都已经被回收,也就是堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

GC算法


GC算法即垃圾回收算法,上面确定了垃圾的判断,现在要对垃圾进行回收。
主要分以下三种算法:

  1. 标记-清除算法
    算法分为“标记”和“清除”两阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续算法都是对它的改进。
    但这种算法存在两个问题:效率低、产生大量不连续的内存碎片。

  2. 标记-复制算法
    为了解决效率问题,复制算法将内存分为大小相同的两块,每次使用其中的一次,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉,这样就使每次内存回收都是对内存区间的一半进行回收。也就是前面堆中介绍的幸存者0和幸存者1区(至于谁是from谁是to看上一次复制方向)
    但每次会有一半空间不可使用。

  3. 标记-压缩算法
    也称为标记-整理算法,让所有的存活对象向一端移动,然后清理掉端边界以外的内存。

    当前虚拟机都采用分代收集算法,就是根据新生代和老年代的特点选择合适的垃圾收集算法。

  • 新生代存活率低,采用复制算法。
  • 老年代存活率高采用清除或压缩算法。

GC收集器


介绍五种GC收集器:

  1. Serial收集器
    Serial(串行)收集器是最基本的垃圾收集器,顾名思义就是一个单线程收集器,不仅仅是只会使用一个线程去收集垃圾,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有线程,直到它收集结束。简单高效,但是又停顿时间,用户体验感差。采用新生复制老年压缩。

  2. ParNew收集器
    ParNew收集器其实就是Seria收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为不变。减少停顿时间,提高用户体验 。采用新生复制老年压缩。

  3. Parallel Scavenge收集器
    类似ParNew收集器,只不过关注点是吞吐量(高效利用CPU),而ParNew关注是用户体验(停顿时间)。该收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。采用新生复制老年压缩。

  4. CMS收集器
    CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了垃圾收集线程和用户线程基本上同时工作。从名字Mark Sweep看出采用的是清除算法。分四步实现:
    ①初始标记:暂停其他线程,标记与root相连对象,速度很快。
    ②并发标记:同时开启GC线程和用户线程,用闭包记录可达对象。
    ③重新标记:重新标记因用户进程产生变动的对象。
    ④并发清除:开启用户线程,同时GC线程清扫标记对象。

  5. G1收集器
    G1(Garbage-Firse)是面向服务器的垃圾收集器,主要针对多处理器即大内存的机器,以极高概率满足GC停顿时间要求的同时,还具备吞吐量性能特征。分为初始标记、并发标记、最终标记和筛选回收。
    G1收集器在后天维护了一个优先队列,每次根据允许的收集时间,优先选择回收价值最大的Region,保证在有限时间内尽可能高的收集效率。

原创不易,请勿转载本不富裕的访问量雪上加霜
博主首页:https://wzlodq.blog.csdn.net/
微信公众号:唔仄lo咚锵
如果文章对你有帮助,记得一键三连❤


转载:https://blog.csdn.net/qq_45034708/article/details/115257271
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场