JVM基础

JVM是Java虚拟机简称,目前最主流使用的JVM叫HotSpot VM,Oracle官方的jdk以及开源的openjdk都是使用了这个JVM。JVM属于Java中比较底层的东西,如果想系统的深入学习JVM,一定要阅读JVM的源码,JVM的源码是用C++写的,这篇博客主要针对面试中的一些重点,通过JVM的内存区域划分,类加载机制,垃圾回收机制来简单介绍一下JVM。

一.JVM内存区域划分

当JVM启动的时候,会申请到一个很大的内存区域,JVM会根据需要把这块空间分成很多个部分,每个部分有不同的功能。下面是分布图:

首先,本地方法栈是JVM内部的C++代码,是给JVM内部的方法准备的栈空间。程序计数器是记录当前线程执行到哪个指令。虚拟机栈是给Java代码使用的栈,此处的栈和数据结构的栈不是一个东西,JVM中的虚拟机栈存储的是方法之间的调用关系,栈空间的每一个元素表示一个方法,把这每一个元素称为一个栈帧,每个栈帧里都会包含这个方法的入口地址,方法参数,返回地址,局部变量等等。当代码中调用一个方法,就会创建栈帧,当方法执行结束后,栈帧就销毁了。栈不是只有一个,而是每个线程有一个。用jconsole就可以查看java进程内部的情况,就可以看到所有的线程的调用栈的情况:

 堆区是整个JVM空间最大的区域,实例化出来的对象,都是在堆上的,类的成员变量也就是在堆上了。堆是一个进程只有一份的。

元数据区,从java8之前叫做方法区,类对象,静态成员就存储在这里。

 二.类加载

类加载的过程准确来说是把.class文件从文件(硬盘)被加载到内存中(元数据区)的过程。大概分为如下几个步骤:

加载过程:也就是把.class文件找到的过程,最终加载完成是要得到类对象。

验证过程:验证过程是检查.class文件格式对不对。.class文件在官方提供的JVM虚拟机规范中详细描述了.class文件的格式。

准备过程:准备过程是给类对象分配内存空间,也就是先在元数据区占个位置。会使元数据区的静态成员被设置成0值。

解析过程:初始化字符串常量,把符号引用转为直接引用。

初始化过程:调用构造方法,进行成员初始化,执行代码块,静态代码块,加载父类等等操作。

一个类什么时候才会被加载呢?类在真正被用到时才加载,类似于懒汉模式。而且只加载一次。比如在构造类的实例中、调用这个类的静态方法,使用静态属性时就会加载。加载子类的同时会先加载其父类。由类加载衍生出一个经典问题:双亲委派模型。双亲委派模型描述的是加载过程中,找.class文件的基本过程。

JVM默认提供了三个类加载器:ApplicationClassLoader 负责加载用户提供的第三方库/用户项目代码中的类、ExtensionClassLoader 负责加载JVM扩展库中的类、BootstrapClassLoader 负责加载标准库中的类。上述三个类存在父子关系,意思就是每个classloader都有一个parent属性,指向父类加载器。上述类加载器如何配合工作的?首先加载一个类的时候,是从ApplicationClassLoader开始,但是由于父子关系,ApplicationClassLoader会把任务交给父亲类ExtensionClassLoader加载,ExtensionClassLoader由于也存在父亲类,就委托给BootstrapClassLoader加载。当BootstrapClassLoader想委托给父亲加载时,发现自己的父亲类是null,所以此时由自己加载。然后BootStrapClassLoader就会搜索自己负责的标准库中的相关的类,如果找到,就加载,如果没找到,就由自己的子类加载器加载。交给ExtensionClassLoader后,它就会搜索扩展库相关的目录,如果找到就加载,找不到交给子类加载器加载。交给ApplicationClassLoader后,它就会搜索用户项目相关的目录,如果能找到就加载,找不到就交给子类加载,但是此时没有子类了,就会抛出类找不到的异常。设定为这个顺序的目的,最主要就是为了保证BootstrapClassLoader可以先加载,ApplicationClassLoader可以后加载,这样就可以避免用户创建了一些奇怪的类引起一些错误。其次,除了这三个jvm自带的类加载器,用户也也可以自定义类加载器,加入到上述流程中,和现有的类加载器配合使用。

三.垃圾回收机制

Java中,垃圾指的就是不再使用的内存了,Java的垃圾回收机制会帮我们把不用的内存自动释放,不同于C/C++,C语言中的malloc操作,C++中的new操作,都是在堆上申请一块内存空间,结束后都需要手动释放内存,如果不手动释放,这块内存的空间就会一直存在直到进程结束(因为堆上的内存生命周期比较长,不像栈会随着方法执行结束自动释放)。如果内存一直占用,就会导致剩余空间不足,进一步导致后续的内存申请操作失败。所以在Java中引入了GC。除了Java,GO,Python,PHP,JS等等主流语言都是使用GC来解决上述问题的。GC主要针对堆内存进行释放,且释放的内存一定是整个对象都不再使用的情况。假如一个对象中的10个属性都不用了,那么就会回收整个对象,如果有一个属性还在用,那么就不回收。

GC实际的工作过程是:

1.找到垃圾/判定垃圾

判定是否是垃圾的思路就是检查到底有没有引用指向它。python/php的做法是引用计数,简单来说就是给每个对象分配一个计数器,每次引用指向该对象,计数器就+1,每次该引用被销毁了,计数器就-1,举个例子:

这个办法简单有效,但是没有用于Java。因为内存空间浪费的多,比如代码中对象很多的情况下,每个对象都需要分配一个计数器。其次存在循环引用的问题。举个例子:

 如果a和b引用被销毁,1号对象和2号对象的引用计数都-1,但是结果还都是1,内存还不能被释放。但是实际上,这两个对象已经不能被访问到了。所以对于Python/PHP采用的垃圾回收方法,还需要搭配其他的机制避免这种问题。

java垃圾回收采用可达性分析的方法。Java中的对象都是通过引用来指向并访问的,经常是一个引用指向一个对象,对象中的成员又指向别的对象。类似上述关系,Java中所有对象通过这种链式/树形结构整体连起来。可达性分析就是把这些被对象组织的结构视为树,从树的根结点出发遍历所有能被访问到的对象,标记为可达,不能访问到的就是不可达。可达性分析需要进行类似树的遍历操作,相比于引用计数来说更慢。

2.清理垃圾:

清理垃圾主要有三种基本做法:

1)标记清除

属于最简单的一种清除方法,把标记为垃圾的对象删除即可。这种方法会存在内存碎片问题,因为被释放的空闲空间是零散的,但是申请内存的要求是连续的空间,所以可能会出现总的空间很大,但是每个具体的空间都很小的情况,导致申请大一点的内存失败。例如,总的空间内存是10K,分为1K一个,一共10个。此时如果申请2K内存就会申请失败了。

2)复制算法

复制算法解决了内存碎片问题。复制算法就是把不是垃圾的对象复制到另外一半内存,然后把整个空间删掉。复制算法解决了内存碎片的问题,但是空间利用率低,并且如果垃圾少,有效对象多,复制成本就会很大。

3)标记整理

标记整理解决了复制算法的缺点,类似于顺序表删除中间元素的操作,既保证了空间利用率,也解决了内存碎片问题。这种做法的缺点也是效率不够高,如果搬运的空间很大,开销也是很大的。

4)分代回收

基于上述几个基本做法研究出了一个复合策略:分代回收。分代回收就在不同的场景使用不同的回收算法。它基于一个规律:如果一个对象存在的时间很长了,那么它大概率还会继续长时间的持续存在下去。这个规律就根据对象生命周期的长短使用不同的算法。这个对象熬过GC的轮次越长,代表他的年龄越大,存在的时间就越久。所以把堆划分成一系列区域,把刚刚创建出来的对象放入划分出的“伊甸区”,当熬过一轮GC后,用复制算法将这个对象放入“幸存区”,放入幸存区后,也要接受GC的考验,变为垃圾就释放,不是垃圾就拷贝到另一个幸存区。两个幸存区是来回进行拷贝的,如果已经拷贝过很多次了,就该进入老年代了。老年代都是在GC中幸存很多次的对象,那么就认为它未来还会长时间幸存,扫描的频率就会变少,如果老年代的对象也成为了垃圾,那么就会用标记整理的方法进行释放。

 上述是GC中典型的垃圾回收算法。事实上JVM有很多的垃圾回收器,回收器具体的实现做法会按照上述算法思想展开,但是会有一些变化/改进。

以上就是JVM一些基础的知识内容,如有错误,欢迎指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晚报大街-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值