JVM(Java虚拟机)简单来讲就是运行Java代码的解释器,做为螺丝钉程序员JVM其实了解下就差很少啦,不懂JVM内部细节照样能写出优质的代码!可是一到造火箭、飞机的场景(面试)不懂JVM的你,会被面试官虐的体无完肤!
面对这一大波JVM面试题,你真的Hold的住吗?java
在面试java工程师的时候,这道题常常被问到,故需特别注意。
Java中的全部类,都须要由类加载器装载到JVM中才能运行。类加载器自己也是一个类,而它的工做就是把class文件从硬盘读取到内存中。在写程序的时候,咱们几乎不须要关心类的加载,由于这些都是隐式装载的,除非咱们有特殊的用法,像是反射,就须要显式的加载所须要的类。
Java类的加载是动态的,它并不会一次性将全部类所有加载后再运行,而是保证程序运行的基础类(像是基类)彻底加载到jvm中,至于其余类,则在须要的时候才加载。这固然就是为了节省内存开销。
Java的类加载器有三个,对应Java的三种类:Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
|
- - ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
|
- - AppClassLoader // 负责加载应用类(程序员自定义的类)三个加载器各自完成本身的工做,但它们是如何协调工做呢?哪个类该由哪一个类加载器完成呢?为了解决这个问题,Java采用了委托模型机制。
委托模型机制的工做原理很简单:当类加载器须要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,若是找不到,才在本身的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,由于加载器必须保证基础类的加载。之因此是这种机制,还有一个安全上的考虑:若是某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,天然就不会将该类加载进来。咱们能够经过这样的代码来获取类加载器:程序员
ClassLoader loader = ClassName.class.getClassLoader(); ClassLoader ParentLoader = loader.getParent();
注意一个很重要的问题,就是Java在逻辑上并不存在BootstrapKLoader的实体!由于它是用C++编写的,因此打印其内容将会获得null。
前面是对类加载器的简单介绍,它的原理机制很是简单,就是下面几个步骤:1.装载:查找和导入class文件;2.链接:(1)检查:检查载入的class文件数据的正确性;(2)准备:为类的静态变量分配存储空间;(3)解析:将符号引用转换成直接引用(这一步是可选的)3.初始化:初始化静态变量,静态代码块。这样的过程在程序调用类的静态成员的时候开始执行,因此静态方法main()才会成为通常程序的入口方法。类的构造器也会引起该动做。面试
JVM内存结构能够大体可划分为线程私有区域和共享区域,线程私有区域由虚拟机栈、本地方法栈、程序计数器组成,而共享区域由堆、元数据空间(方法区)组成。
再有人问你JVM的内存结构就回想下上面的图,可是知道JVM的内存模型的样子仍是不行的,还要知道他们分别干什么的。算法
当你碰到过StackOverflowException这个异常的时候,有没有思考下为何会出现这样的异常呢?答案就在虚拟机栈中,JVM会为每一个方法生成栈帧而后将栈帧压入虚拟机栈中。举个粟子:假设JVM参数-Xss设置为1m,若是某个方法里面建立一个128kb的数组,那这个方法在同一个线程中只能递归4次,再递归第五次的时候就会报StackOverflowException异常,由于虚拟机栈的大小只有1m,每次递归都须要为方法在虚拟机栈中分配128kb的空间,很显示到第五次的时候就空间不足了。数组
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。JVM的多线程是经过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程当中可能会由于时间片耗尽而被挂起,而另外一个线程获取到时间片开始执行。简单的说程序计数器的主要功能就是记录着当前线程所执行的字节码的行号指示器。安全
方法区存储了类的元数据信息、静态变量、常量等数据。多线程
日常你们使用new关键字建立的对象都会进入堆中,堆也是GC重点照顾的区域,堆会被划分为:新生代、老年代,而新生代还会被进一步划分为Eden区和Survivor区:
新生代中的Eden区和Survivor区,是根据JVM回收算法来的,只是如今大部分都是使用的分代回收算法,因此在介绍堆的时候会直接将新生代概括为Eden区和Survivor区。app
JVM内存模型小结:jvm
JVM内存模型划分为线程私有区域和共享区域ide
虚拟机栈/本地方法栈负责存放线程执行方法栈帧
程序计数器用于记录线程执行指令的位置
方法区(元数据区)存储类的元数据信息、静态变量、常量等数据
堆(heap)使用new关键字建立的对象都会进入堆中,堆被划分为新生代和老年代
JVM判断对象回收有两种方式:引用记数、GC Roots,引用记数比较简单,JVM为每一个对象维护一个引用计数,假设A对象引用计数为零说明没有任务对象引用A对象,那A对象就能够被回收了,可是引用计数有个缺点就是没法解决循环引用的问题。GC Roots经过一系列的名为GC Roots的对象做为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证实对象是不可用的。在Java中,能够做为GC Roots的对象包括下面几种:
虚拟机栈中引用的对象;
方法区中类静态属性引用的对象;
方法区中的常量引用的对象;
本地方法栈中JNI(即通常说的Native方法)的引用的对象;
总的来讲就是当一个对象经过GC Roots搜索不到时,说明对象能够被回收了,但何时回收还要看GC的心情!
这种算法分两步:标记、清除两个阶段, 标记阶段是从根集合(GC Root)开始扫描,每到达一个对象就会标记该对象为存活状态,清除阶段在扫描完成以后将没有标记的对象给清除掉。
用一张图说明:
这个算法有个缺陷就是会产生内存碎片,如上图B被清除掉后会留下一块内存区域,若是后面须要分配大的对象就会致使没有连续的内存可供使用。
标记整理就没有内存碎片的问题了,也是从根集合(GC Root)开始扫描进行标记而后清除无用的对象,清除完成后它会整理内存。
这样内存就是连续的了,可是产生的另一个问题是:每次都得移动对象,所以成本很高。
复制算法会将JVM推分红二等分,若是堆设置的是1g,那使用复制算法的时候堆就会有被划分为两块区域各512m。给对象分配内存的时候老是使用其中的一块来分配,分配满了之后,GC就会进行标记,而后将存活的对象移动到另一块空白的区域,而后清除掉全部没有存活的对象,这样重复的处理,始终就会有一块空白的区域没有被合理的利用到。
两块区域交替使用,最大问题就是会致使空间的浪费,如今堆内存的使用率只有50%。
JVM回收算法小结:
标记清除速度快,可是会产生内存碎片;
标记整理解决了标记清除内存碎片的问题,可是每次都得移动对象,所以成本很高;
复制算法没有内存碎片也不须要移动对象,可是致使空间的浪费;
新建立出来的对象一开始都会停留在新生代中,但随着JVM的运行,有些存活的长的对象会慢慢的移动到老年代中。
JVM会给对象增长一个年龄(age)的计数器,对象每“熬过”一次GC,年龄就要+1,待对象到达设置的阈值(默认为15岁)就会被移移动到老年代,可经过-XX:MaxTenuringThreshold调整这个阈值。
一次Minor GC后,对象年龄就会+1,达到阈值的对象就移动到老年代,其余存活下来的对象会继续保留在新生代中。
根据对象年龄有另一个策略也会让对象进入老年代,不用等待15次GC以后进入老年代,他的大体规则就是,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就能够直接进入老年代了。
如图上的A、B、D、E这四个对象,假如Survivor 2是100m,若是A + B + D的内存大小超过50m,如今D的年龄是10,那E都会被移动到老年代。实际上这个计算逻辑是这样的:年龄1 + 年龄2 + 年龄n的多个对象总和超过Survivor区的50%,那就会把年龄n以上的对象都放入老年代。
若是设置了-XX:PretenureSizeThreshold这个参数,那么若是你要建立的对象大于这个参数的值,好比分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会通过新生代。这么作就能够避免大对象在新生代,多次躲过GC,还得把他们来复制来复制去的,最后才进入老年代,这么大的对象来回复制,是很耗费时间的。
JVM在发生Minor GC以前,虚拟机会检查老年代最大可用的连续空间是否大于新生代全部对象的总空间,若是大于,则这次Minor GC是安全的若是小于,则虚拟机会查看HandlePromotionFailure设置项的值是否容许担保失败。若是HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,若是大于则尝试进行一次Minor GC,但此次Minor GC依然是有风险的;若是小于或者HandlePromotionFailure=false,则改成进行一次Full GC。
将前面的一些问题总结下来,而后应用到线上,那JVM应该如何优化减小Full GC呢?以标准的4核8G机器为例说明,首先系统预留4G,其余4G按以下规则分配 :
堆内存:3g
新生代:1.5g
新生代Eden区:1228m
新生代Survivor区:153m
方法区:256m
虚拟机栈:1m/thread
设置参数以下:
-Xms3072m -Xmx3072m -Xmn1536m -Xss=1m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:HandlePromotionFailure -XX:SurvivorRatio=8
在优化JVM以前,要先估算要系统每秒占用的内存数量,若有个日活百万的商场系统,每日下单量在20w左右,按照一天8个小时算,那订单服务的每秒大概会有500个请求,而后粗略的估算下每一个请求占用多少内存,计算出每秒要花费多少内存。假设是每秒500个请求,每一个请求须要分配100k的空间,那1秒须要分配大约50m的内存。
按照以前的估算1秒须要分配大约50m的内存的话,Eden区的空间是1228m那平均每25秒就要执行一次Minor GC。
按照上面的模型,每25秒就要执行一次Minor GC,GC执行期间并不能回收掉全部的新生代中的对象,那每秒50m那每次GC执行期间还会剩下大约100m没法回收的对象会进入Survivor区,可是别忘记JVM有动态年龄判断机制,这样设置下来Survivor的空间明显小了一点,因此将新生代设置2048m,才能避免触发动态年龄判断:
-Xms3072m -Xmx3072m -Xmn2048m ...
大对象通常是长期存活和使用的对象,通常来讲设置1M的对象直接进入老年代,这样避免大对象一直处于新生代中来回复制,因此加上PretenureSizeThreshold=1m参数。
... -XX:PretenureSizeThreshold=1m ...
Minor GC后默认躲过15次垃圾回收后自动升入老年代,按照咱们的评估25秒触发一次Minor GC,若是按照MaxTenuringThreshold参数的默认值,躲过15次GC后,应该是6分钟以后的事了,结合当前业务场景这里能够下降一点,让那些本应该进入老年代的对象,尽快的进入老年代,避免复制成本和浪费新生代空间,从而致使新生代Survivor空间不足,引起Full GC。
... -XX:MaxTenuringThreshold=6 ...
JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,class 类信息常量池(static 常量和 static 变量)等放在方法区new:方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数堆:初始化的对象,成员变量 (那种非 static 的变量),全部的对象实例和数组都要在堆上分配栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操做数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,因此仍是一个指向地址的指针本地方法栈:主要为 Native 方法服务程序计数器:记录当前线程执行的行号
全部的面试题目都不是一成不变的,特别是像一线大厂,上面的面试真题只是给你们一个借鉴做用,最主要的是给本身增长知识的储备,有备无患。
给你们分享整理的2019年大厂JVM面试题资料(20多页pdf文档)以及多家公司java面试题资料100多页pdf文档和各知识点学习路线思惟脑图(xmind)还有JVM讲解视频。欢迎你们关注个人公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。但愿这些资料对你们有所帮助!
欢迎你们一块儿交流,喜欢文章记得关注我点个赞哟,感谢支持!