JVM

作者: 与搬砖有关的日子 | 来源:发表于2018-11-22 11:03 被阅读9次

    1.   JVM基本概念

    JVM是可运行Java代码的假象计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收、堆和一个存储方法域。JVM是运行在操作系统之上的它与硬件没有直接的交互。

    2.   Jvm原理

    运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,他就会被解释执行或者是被即时代码发生器有选择的转换成机器码执行。

    JVM在它的生命周期中的任务就是运行Java程序,因此当Java程序启动的时候就产生JVM的一个实例,当程序运行结束的时候该实例也就消失了。JVM是程序与底层操作系统和硬件无关的关键。

    3.    JVM体系结构

    (1)  Class Loader类加载器

    负责加载.class文件,class文件在文件开头有特定的文件标识,并且ClassLoader负责class文件的加载等,至于它是否可以运行则由Execution Engine(执行引擎)决定。作用有定位和导入二进制class文件;验证导入类的正确性;为类分配初始化内存;帮助解析符号引用。

    (2)   Native Interface本地接口

    本地接口的作用是融合不同的编程语言为Java所用。

    (3)   Execution Engine执行引擎

         执行包在装载类的方法中的指令,也就是方法。

    (4)   Runtime data area运行数据区

         虚拟机内存或者Jvm内存在整个计算机内存中开辟一块内存存储Jvm需要用到的对象变量等,运行区又分很多小区,分别为:方法区,虚拟机栈,本地方法栈,堆,程序计数器。

    4.   JVM数据运行区详解(栈管运行,堆管存储)

    说明:JVM调优主要是优化Heap堆和Method Area方法区。

    1)    Native Method Stack本地方法栈

    它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。当线程调用java方法时,虚拟机会创建一个新的栈帧并压入Java栈,然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再有线程的Java栈中压入新的帧,虚拟机只会简单的动态链接并直接调用指定的本地方法。例如某个虚拟机实习的本地方法接口时使用C连接模型的话,那么它的本地方法就是C栈。

    (2)    PC Register程序计数器

    每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(下一个要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

    (3)   Method Area方法区

    方法区是被所有线程共享,所有字段和字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说所有定义的方法的信息都保存在该区域,此区域属于共享区间。静态变量、常量、类信息、运行时常量存在方法区中,实例变量存在堆内存中。方法区有时被称为持久代(PermGen)。

    (4)   Stack栈

    栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是随着线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。栈主要存储3类数据:本地变量,输入参数和输出参数以及方法内的变量;虚拟机只会对栈进行两种操作,记录出栈、入栈的操作;栈帧数据,包括类文件、方法等。

    栈中数据都是以栈帧的格式存在,栈帧是一个内存区域是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法被调用时就产生了一个栈帧被压入到栈中。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

    这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

    (5)   Heap堆

    堆这块区域是JVM中最大的,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,这块区域也是线程共享的,是gc主要的回收区,一个JVM实例只存在一个堆类存,堆的大小是可以调节的。类加载期读取了类文件后,需要把类、方法、常变量放到堆内存中。堆内存分为三部分:

    新生区:新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集结束生命。新生区又分为两部分:伊甸区和幸存区,所有的类在伊甸区被new出来。幸存区有两个区0区和1区。当伊甸区的空间用完时,程序又需要创建对象,JVM的垃圾回收器对伊甸区进行垃圾回收,将伊甸区中剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。如果1区也满了再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullFC),进行养老区内存清理。若养老区执行FullGC之后发现依然无法进行对象的保持,就会产生OOM(OutOfMemoryError)异常。

    养老区:养老区用于保存从新生区筛选出来的Java对象,一般池对象都在这个区域活跃。

    永久区:永久存储区是一个常驻内存区域,用于存放JDK自身携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域占用的内存。

    如果出现java.lang.OutOfMemoryError:PermGen space,说明Java虚拟机对永久代Perm内存设置不够。原因有二:a.程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。b.大量动态反射生成的类不断被加载,最终导致Perm区被占满。

    方法区和堆内存的异议:实际而言,方法区和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息、普通常量、静态常量、编译器编译后的代码等。JVM规范将方法区描述为堆的一个逻辑部分。

    堆跟栈的区别:堆存的是对象和数组所以堆的大小不是固定的是运行时确定,栈存的是局部变量和操作数所以栈的大小是固定的在编译期确定。

    5.   Minor GC ,Full GC 触发条件

    Minor GC触发条件:当Eden区满时,触发Minor GC。

    Full GC触发条件:

    (1)调用System.gc时,系统建议执行Full GC,但是不必然执行

    (2)老年代空间不足

    (3)方法去空间不足

    (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

    (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

    6.   GC常用算法

    判断对象是否存活一般有两种方式:

    引用计数算法:此对象有一个引用则+1,删除一个引用则-1,只用收集计数为0的对象。缺点是a.无法处理循环引用的问题。B.引用计数的方法需要编译器的配合,编译器需要为此对象生成额外的代码。

    根搜索算法:建立若干中根对象,当任何一个根对象到某一个对象均不可达时则认为这个对象是可以被回收的。根对象一般为虚拟机栈中的引用的对象,方法区中的类静态属性引用的对象,方法区中的常量引用的对象,本地方法栈中的引用对象。

    在根搜索算法的基础上,现代虚拟机的实现当中垃圾搜集算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法。

    标记-清除算法:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是清除。标记的过程就是遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。缺点是效率低(递归与全堆对象遍历),清理出来的空间不连续,在进行GC的时候要停止应用程序。

    复制算法:将内存分为两个区间,在任意时间点所有动态分配的对象都只能分配在其中一个区间。当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程,将活动区间的存活对象全部复制到空闲空间,且严格按照内存地址一次排列,与此同时,GC线程将更新存货对象的内存引用地址指向新的内存地址。此时空闲空间与活动区间交换。缺点是浪费了一般内存,如果对象的存活率很高,将很浪费。

    标记-整理算法:当堆中的有效内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项是整理。标记的过程就是遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。整理的过程就是移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。

    分代搜集算法:新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

    7. JVM中一次完整的GC流程是怎样的?

    对象优先在新生代区中分配,若没有足够空间,Minor GC;大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。如果对象在新生代出生并经过第一次MGC后仍然存货年龄+1,若年龄超过一定限制(15)则被晋升到老年态。

    8. 你知道几种垃圾收集器,各自优缺点。

    CMS垃圾收集器(优点: 并发收集、低停顿 缺点: 产生大量空间碎片、并发阶段会降低吞吐量)的四个步骤:

    a.初始标记(需要暂停)仅仅标记GCroot能直接关联的对象

    b.并发标记(不需要暂停)GC Roots Tracing的过程

    c.重新标记(需要暂停)修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

    d.并发清除(不需要暂停)

    G1垃圾收集器:

    特点是空间整合,采用标记-整理算法不会产生空间碎片;可预测停顿。

    将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

    9. Java中基本变量、对象、对象引用等在堆与栈中存储、按值传递机制、栈中对象共享机制

    Java中只有一种参数传递方式那就是按值传递,即Java中传递任何东西都是传值。如果传入方法的是基本类型你就得到基本类型的一份拷贝。如果是传递引用就得到引用的拷贝。Java的堆是一个运行时数据区,类的对象从中分配空间。堆是由垃圾回收负责的,堆的优势是可以动态分配内存大小。栈的优势是存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。例子:int a = 3; int b = 3;编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到就将3这个值存放进来然后将a指向3。接着处理int b = 3;在创建完b的引用变量后因为在栈中已经有3这个值,便将b指向3。这样a和b同时指向3。

    10.    强软弱虚—强引用、软引用、弱引用、虚引用

    强引用:new出的对象之类的引用,只要引用还在永远不会回收。

    软引用:引用但非必须的对象,内存溢出异常之前回收。

    弱引用:非必须的对象,对象能生存到下一次垃圾收集发生之前。

    虚引用:对生存时间无影响,在垃圾回收时得到通知。

    强引用:例如Object object = new Object();那么object就是一个强引用,垃圾回收器绝不会回收它,当内存不足时java虚拟机宁愿抛出OutOfMemoryEoor错误,也不会回收强引用对象来解决内存不足的问题。

    软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

    弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

    11. 对Java内存模型的理解,以及其在并发中的应用。

    Java线程之间的通信由Java内存模型控制。

    所有变量的存储在主内存,每个线程还都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作必须在工作内存完成,而不能直接读取内存中的变量。不同的线程直接无法访问对方工作内存中的变量,线程间变量的传递需要通过主内存来完成。

    12.  堆内存溢出:

    导致OutOfMemoryError异常的原因:

    内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

    集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

    代码中存在死循环或循环产生过多重复的对象实体;

    启动参数内存值过小。

    a.   java.lang.OutOfMemoryError:Java heap space:Java堆内存不够,一个原因是真不够,一个原因是程序中有死循环。

    b.   java.lang.OutOfMemoryError:GC overhead limit exceeded:当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,没有足够的内存。

    c.    java.lang.OutOfMemoryError:PermGen

    space:这种是P区内存不够,可通过调整JVM的配置

    JVM的Perm区主要用于存放Class和Meta信息,Class在被Loader时就会被放到PermGen space,这个区域称为年老代,GC在主程序运行期间不会对年老区进行清理,默认是64M大小,当程序需要加载的对象比较多时,超过64M就会报这部分内存溢出了,需要加大内存分配,一般128M足够。

    d.   java.lang.StackOverflowError:线程栈的溢出,要么是方法调用层次过多,要么是线程栈太小。通过-Xss参数增加线程栈的大小。比如说太深的递归。递归函数会不断的占用栈空间。

    13.  Java异常

    a.   Throwable:

    Throwable是Java语言中所有错误或异常的超类。

    Throwable包含两个子类:Error和Exception。它们通常用于指示发生了异常情况。

    Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。

    b.   Exception:(被检查的异常)

    Exception及其子类是Throwable的一种形式,它指出了合理的应用程序想要捕获的条件。Java编译器会检查它,此类异常要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。

    c.    RuntimeException(运行时异常)

    那些可能在Java虚拟机正常运行期间抛出的异常的超类。如果代码会发生RuntimeException异常,则需要通过修改代码进行避免。例如除数为零。Java编译器不会检查它,也就是出现这类异常时,倘若没有通过throws声明抛出它也没有try-catch捕获它还是会编译通过。虽然java编译器不会检查运行时异常但是我们也可以通过throws进行声明抛出。

    d.    Error

    用于指示合理的程序不应该视图捕获的严重问题。编译器不会对错误进行检查。程序本身无法修复这些错误。

    14.JVM的调优:

        a. 将新对象预留在年轻代;    b.让大对象进入年老代;    c. 设置对象进入年老代的年龄    d. 尝试使用大的内存分页;   

         e. 增大吞吐量提升系统性能;    f. 年老代年轻代大小划分     g.内存泄漏     h.垃圾回收算法设置合理

    15.为什么分代?

    将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

    相关文章

      网友评论

        本文标题:JVM

        本文链接:https://www.haomeiwen.com/subject/sonkqqtx.html