后端面试38讲_04_03丨Java虚拟机原理JVM为什么被称为机器machine
你好,我是李智慧。
人们呢常说java是一种跨平台的语言,这就意味着java开发出来的程序经过编译后,可以在linux上运行,也可以在windows上运行,可以在PC服务器上运行,也可以在手机上运行,可以在x八六的CPU上运行,也可以在arm的CPU上运行。
这一特性是不是很神奇?因为不同的操作系统,特别是不同的CPU架构,是不可能执行相同的指令的。
而java之所以能够做到,是因为java编译的自节码文件,不是直接在底层的操作系统平台上运行呢,而是在java虚拟机JVM上运行的JVM呢屏蔽了底层系统的不同,为java自节码文件构造了一个统一的运行环境。
所以呢JVM其实本质上也是一个应用程序。
启动以后呢,加载执行java的字节码文件,JVM的全称是java vtuo machine.那么你有没有想过这样一个程序,为什么被称为机器machine呢?其实如果回答了这个问题,也就回答了JVM的底层构造了。
而了解了JVM的底层构造呢。
在进行java开发的时候啊,如果遇到各种问题,不妨思考一下在JVM层面是如何的呢?然后可以进一步查找资料分析问题,直至真正的解决问题。
那么我们就看看GVM的底层构造究竟是如何的。
Gvm主要有类加载器运行时数据区和执行引擎三部分。
组成。
运行时数据区呢主要包括方法区堆、java站和程序计术寄存器。
方法区呢主要存放从硬盘加载进来的点class内置解码。
而在程序运行过程中创建的类实例在存放在堆中程序运行的时候呢,实际上是以线程为单位运行的当JVM进入启动类的。
没有方法的时候啊,就会为应用程序创建一个主线程。
每一方法里的代码就会被这个主线程执行。
每个线程有自己的java栈,栈里存放着方法运行器的局部变量。
而当前线程执行到哪一行质检码指令这个信息呢则被存放在程序技术寄存器里面。
一个典型的java程序运行过程是下面这样的,通过java命令启动JVMJVM的类加载器呢。
根据java命令的参数,到指定的类路径下加载表,class类文件类文件被加载到内存后呢,存放在专门的方法区。
然后JVM创建一个主线层执行这个类文件的main方法,main方法输入参数和方法内定义的变量被压入到java栈。
如果在方法内创建了一个对象实例,这个对象实例信息将会被存储到堆里。
而方法实例的引用,也就是对象实例在堆中的地址信息则会被记录在栈里堆中。
记录的对象实例信息呢主要是成员变量信息,因为类方法内的可执性代码存放在方法区,而方法内部的局部变量依然存放在现成的栈里。
程序计术寄存器呢一开始存放的就是main方法的第一行代码。
Java的执行引擎呢,根据这个位置取方法区的对应位置加载这行代码指令,将其解释为自身平台的CPU指令后交给CPU执行。
如果们以方法里调用了其他方法,那么在进入其他方法的时候呢,会在java栈中为这个方法创建一个新的战争。
当线程在这个方法内执行的时候,方法内的局部变量都存放在这个战争里。
当这个方法执行完毕,退出的时候啊,就把这个战争从java栈中出战。
这样当前战争也就是堆栈的栈点,又回到了魅in方法的战争。
使用这个战争中的变量继续执行魅in方法。
这样即使main方法和f方法都定义了相同的变量,java也不会弄错了这部分内容,我们在第一篇已经讨论过了。
Java作为一个码性呢操和操作系统处理线程站的方法其实是一样的。
你可能有时候会疑惑,java的线程安全究竟是怎么回事呢?到底什么样的数据是现场安全的,什么样的数据是线程不安全的呢?其实你可以试着从java栈的角度去理解,所有在方法内定义的基本类型变量,都会被每个运行在这个方法的线程放入到自己的栈中,线程的栈彼此隔离。
所以这些变量一定是线程安全。
如果在方法里创建了一个对象实例,这个对象实例没有被方法返回,或者放入到胃部的对象容器中的话,也就是说这个对象的引用没有离开这个方法。
虽然这个方法被放置在堆中,但是这个方法不会被其他现场访问到,也是现场安全的。
相反,像受累的这样的类呢,在外部容器中创建以后会被传递。
在每个访问应用的用户线程执行。
这个类就不是现场安全的。
但一个类实例被多个线程访问,并不意味着一定会引发现场安全问题。
如果serverelt类里没有成员变量,即使多线程同时执行这个serverlight实例的方法,也不会造成成员变量冲突,也是现场安全的。
这种对象呢就被称作是无状态对象。
也就是说,对象不记录状态,执行这个对象的任何方法都不会改变对象的状态,也就不会有现场安全问题了。
事实上啊,在web开发实践中,常见的service类、RDIO类r都被设计成无状态对象。
所以虽然我们开发的web应用都是多线程的应用,因为b容器一定会创建多线程来执行我们的代码。
但是呢我们开发中却可以很少考虑线程安全的问题。
那我们再回过头来看看JVM,它分装了一组自定义的自解码指令集,它自己的程序、计数器和执行引擎,像CPU一样可以执行运算指令。
它还像操作系统一样,有自己的程序装载与运行机制,内存管理机制、线程以及站的管理机制。
看起来啊就像是一台完整的计算机。
这回你应该明白为什么JVM被称作马信的原因了吧。
事实上啊,GVM比操作系统更进一步,它不但可以管理内存,还可以对内存进行自动垃圾回收。
所谓自动垃圾回收,就是将JVM堆踪那些已经不再使用的对象清理掉,释放宝贵的内存资源。
那么问题来了,要想进行垃圾回收,如何知道哪些对象是不再使用可以清理的呢?事实上呢,JVM通过一种可达性分析算法,进行垃圾对象的识别。
具体过程呢是这样的,也现成战争中的局部变量或者是方法区的静态变量出发,将这些变量引用的对象进行标记。
然后看这些被标记的对象是否引用的其他对象继续进行标记。
所有被标记过的对象呢都是可达的,从那是在被使用的对象。
而那些没有被标记的对象啊,就是不可达的可回收的垃圾对象的。
所以你可以看出来,可达性分析算法呢其实是一个引用标记。
算法进行完引用标记以后呢,JVM就会对垃圾对象占用的内存进行回收。
回收主要有三种方法,第一种方法呢是清理将垃圾对象占据的内存清理掉。
其实,GVM并不会真的将这些垃圾对象内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲记录。
在一个空闲列表里。
当应用程序需要创建新对象的时候呢,就从空闲列表找一段空闲内存分配给这个新对象。
但是这样做呢有个很明显的缺陷,因为垃圾对象是散落在内存空间各处的,所以标记出来的空闲空间也是不连续的当应用程序。
要创建一个数组,需要申请一段连续的大内存空间的时候呢,就算堆空间中有足够的空闲空间,也无法为应用程序分配内存了。
第二种方法呢是压缩从堆空间的头部开始,将存活的对象拷贝放在一段连续的内存空间中,看起来好像是对象被压缩了,这样其余的控件就是连续的空闲空间了。
第三种方法呢就是复制,将堆空间分成两部分,只在其中一部分创建对象。
当这个部分控件用完的时候呢,就将标记过的可用对象复制到另一个控件。
中GMM将这两个空间分别命名为from区和to区。
当对象。
从from区域复制到to区域以后呢,这两个区域交换名称引用,也就是from区变成to区to区变成form区,从而保证吐局总是空的。
Cvm在进行垃圾回收的时候呢,会进行分代垃圾回收。
绝大多数的java对象存活时间都非常短。
很多时候呢就是在一个方法内,创建对象对象放在栈中。
当方法调用结束战争出战的时候呢,这个对象就失去引用了,成为垃圾。
针对这种情况,JVM将堆空间分成新生代和老年代两个区域创建对象的时候呢,只在新生代创建。
当新生代空间不足的时候呢,只对新生代进行垃圾回收,这样需要处理的内存空间就比较小,垃圾回收速度呢就比较快。
新生代又分为一等区from区和too取三个区域,每次创建对象都是在一等区创建,每次垃圾回收的时候都是扫描一等区from区,每次重合对象复制到吐取,然后交换from区和too取的名称。
引用下次垃圾回收时候呢,继续将存货对象从from去复制到two去。
当一个对象经过几次新生代垃圾回收以后呢,也就是几次从from区域复制到凸区以后呢,依然存活。
那么这个对象就会被复制到老年代区域了当老年代区域已满,也就是无法将新生代中多次复制以后依然存活的对象。
复制进去的时候呢,就会对新生代和老年代的内存空间进行一次全量垃圾回收。
所以呢根据应用程序的对象存活时间,合理设置老年代和新生代的内存空间比例,对GVM垃圾回收的性能有很大的影响。
Gvm中具体执行垃圾回收的垃圾回收器有四种,第一种呢是sirr,串行垃圾回收,这是GVM.早期的垃圾回收器,只有一个线程执行垃圾回收。
第二种呢是parallel并行垃圾回收器,它启动多线程执行垃圾回收。
如果GVM运行在多核CPU上,那么显然并行垃圾回收要比串行垃圾回收效率高。
在串行和并行垃圾回收过程中呢,当垃圾回收线程工作的时候,必须要停止用户线程的工作,否则就可能会导致对象的引用标记错了。
因此,垃圾回收过程也被称作是stopper world,在用户视角看来,所有的程序都不再执行了,整个世界都停止了。
第三种呢是CMS并发垃圾回收器。
在垃圾回收的某些阶段,垃圾回收线程和用户线程可以并发运行。
因此对用户现场的影响较小。
Web应用之类对用户响应时间比较敏感的场景呢就适用CMS垃圾回收器。
最后一种呢是季旺垃圾回收器,它将整个堆空间分成多个子区域。
然后呢,在这些子区域上各自独立进行垃圾回收。
在回收过程中的某些阶段,垃圾回收线程和用户线程也是并发用行极旺。
综合了前几种垃圾回收器的优势,适用于各种场景是未来主要的垃圾回收器。
我们为什么需要了解JVM呢? Jvm有很多配置参数,java开发过程中也可能会遇到各种问题,了解GVM的基本构造,就可以帮助我们从原理上解决问题。
比如遇到auto、 marry error这种异常的时候呢,我们就知道是堆空间不足了。
那可能是GVM分配的内存空间,不足以让程序正常运行,也可能是程序内存存在泄露了。
比如一些对象被放入到list或者map的容器对象中,虽然程序已经不再使用这些对象了,但是这些对象依然被容器对象引用,没有办法进行垃圾回收,导致内存泄露。
这个时候呢就可以通过g map命令查看堆中的对象情况,分析是否有内存泄露。
如果遇到steck over floor error,我们就知道是占空件不足了。
战空间不足呢,通常是因为方法调用的层次太多了,导致战争太多。
我们可以先通过战争异常信息观察是否存在错误的递归调用。
因为每次递归调用都会使切头方法调用更深一层。
如果调用是正常的呢,可以尝试调整XSS参数,增加占空间的大小。
如果程序运行卡顿部分请求响应延迟比较厉害,那么可以通过g steate命令查看垃圾回收器的运行状况,是否存在较长时间的four GC.然后调整垃圾回收器的相关参数,使垃圾回收对程序运行的影响尽可能的小执行引擎。
在执行自检码指令的时候呢,是解释执行的。
也就是说,每个质检码指令都会被解释成一个底层的CPU指令。
但是这样的解释执行效率比较差,JVM呢专门进行了优化,将频繁执行的代码编译为底层的CPU指令重组起来。
后面在执行的时候呢,就直接执行编译好的指令,不再解释执行,这就是JVM的即时编译GIT.而b应用程序通常是尝试间用行的,使用,GIT呢有很好的优化效果,可以通过高server参数打开GIT的CR编译器进行优化。
总之呢,你理解了JVM的构造,在进行java开发的时候,遇到各种问题都可以思考一下,这在JVM层面是如何的呢?然后进一步的查找资料分析问题,这样就会真正的解决问题。
而且经过这样不断的思考分析,对java、对JYM,甚至对整个计算机的原理体系以及设计理念都会有更多的认识和领悟。
你在java开发过程中遇到过什么样的问题呢?这些问题和GMM的底层原理是怎样的关系呢?你有想过这些问题吗?你可以把你的疑惑或者想法写在评论区里集思广益,也欢迎把这篇文章分享给你的朋友,或者同事一起交流一下。