java深入篇之JVM

作者: 千淘萬漉 | 来源:发表于2018-02-10 17:09 被阅读815次

    一、参数

    1.说说你知道的几种主要的jvm 参数

    情景开放的题目,在实际情况中可以根据项目需要来数量掌握几个jvm参数,简单摘录了如下:

    -server -Xmx3g -Xms3g -XX:MaxPermSize=128m 
    -XX:NewRatio=1  eden/old 的比例
    -XX:SurvivorRatio=8  s/e的比例 
    -XX:+UseParallelGC 
    -XX:ParallelGCThreads=8  
    -XX:+UseParallelOldGC  这个是JAVA 6出现的参数选项 
    -XX:LargePageSizeInBytes=128m 内存页的大小, 不可设置过大, 会影响Perm的大小。 
    -XX:+UseFastAccessorMethods 原始类型的快速优化 
    -XX:+DisableExplicitGC  关闭System.gc()
    

    如果需要详细的参数信息请戳下面:

    《JVM参数配置大全》

    2.-XX:+UseCompressedOops 有什么作用?

    当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。这也会对 CPU 缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小,通过压缩 OOP可以节省一定的内存。通过

     -XX:+UseCompressedOops
    

    选项,JVM 会使用 32 位的OOP,而不是 64 位的 OOP

    二、类加载器(ClassLoader)

    1.Java 类加载器都有哪些?

    虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)。在类加载的5个过程中,类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。

    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

    系统(System)类加载器,也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

    《深入理解Java类加载器(ClassLoader)》

    补充:双亲委派模型
    从Java虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。

    双亲委派模型流程

    某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

    使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

    2.JVM如何加载字节码文件?

    每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载

    类加载
    加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
    验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
    准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
    解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。
    初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

    三、内存管理

    1.JVM内存分哪几个区,每个区的作用是什么?

    Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

    内存划分
    1.程序计数器
    JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示执行哪条指令的。在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
    2.java栈
    Java栈是Java方法执行的内存模型, Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。
    其中最重要的就是局部变量表,就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
    java栈

    由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。
    3.本地方法栈
    本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
    4.堆
    Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。
    5.方法区
    方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

    在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

    在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

    在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

    《JVM的内存区域划分》

    2.一个对象从创建到销毁都是怎么在这些部分里存活和转移的

    (1)对象的创建:

    • 定位到方法区常量池的类,检查类是否已经被加载、验证、准备、解析。
    • 检查类是否已经进行过初始化。
    • 在内存中分配空间
    • 设置对象的默认初始值(0,false,null)
    • 执行<clint>()方法(类变量的赋值和静态代码块)
    • 执行构造
    • 在线程栈中设置对象引用

    (2)对象在内存中的布局:
    对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。

    • 对象头(Header):每一个堆中的对象在HosPost虚拟机中都有一个对象头(ObjectHeader),对象头里存储两部分信息:一部分是运行时对象自身的哈希码值(HashCode)、GC分代年龄(Generational GC Age)、锁标志位(占用2个bit的位置)等信息,另一部分是指向方法去中的类型信息的指针。如果是数组对象的话,还会有额外一部分用来存储数组长度。
    • 实例数据:是程序代码中定义的各种类型的字段内容。
    • 对齐填充:并不是必然存在的,仅仅在对象的大小不是8字节的整数倍时起占位符的作用。

    (3)对象的访问方式:1,句柄;2,直接指针
    句柄:java堆中划出一块空间单独存放句柄池,java栈中的reference指向句柄池中的某个句柄,句柄包含两部分信息:堆中实例对象地址和方法区中对象类型信息。
    直接指针:java栈中的reference指向java堆中的实例对象,每个实例对象上有一部分信息是用来指向该对象在方法区中的类型信息。
    句柄的优势:当对象发生改变时,reference的值不用变,只需要改变句柄的实例指针,reference自身不需要改变。直接指针的优势:节省了一次指针定位的开销,速度更快。
    HosPot虚拟机采用第二种方式。

    (4)销毁
    对象完成使命后,等待GC进行垃圾回收。销毁对象即清理对象所占用的内存空间,会调用对象的finalize()方法。

    3.解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法

    堆区:
    1.存储的是new出来的对象和数组,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
    2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

    栈区:
    1.每个线程包含一个栈区,栈中保存的是所有的变量,包括基本类型和引用类型,栈中的每个变量都包含类型、名称、值这些内容,只不过基本类型变量的值为一个具体的值,而引用类型的变量的值为对象在堆中的地址。
    2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
    3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

    方法区:
    1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
    2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
    3.字符串常量池就是存放在方法区。

    java 堆(heap)、栈(stack)和方法区(method)

    4.JVM中哪个参数是用来控制线程的栈堆栈小

    -Xss参数用来控制线程的堆栈大小。你可以查看JVM配置列表来了解这个参数的更多信息。该篇文章还涉及到其他不错的jvm调优策略,推荐阅读。

    5.简述内存分配与回收策略

    JVM采用分代的垃圾回收策略:不同对象的生命周期是不一样的。目前JVM分代主要是分三个年代:
    新生代:所有新创建的对象都首先在新生代进行内存分配。新生代具体又分为3个区,一个Eden区、一个From Survivor区和一个To Sruvivor区。大部分对象都被分配在Eden区,当Eden区满时,还存活的对象将被复制到From Survivor区,当From Survivor区满时,此区还存活的对象将被复制到To Survivor区。最后,当To Survivor区也满时,这时从From Survivor区复制过来并且还存活的对象将被复制到老年代。
    老年代:在年轻代中经历了N次(一般是15次)GC后依然存活的对象,就会被放到老年代当中。因此,可以认为老年代是存放一些生命周期较长的对象。
    持久代:用于存放静态文件,如Java类等。

    内存模型图

    内存分配与回收策略:

    • 对象优先在Eden分配。在大多数情况下,对象在新生代的Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
    • 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间的情况下就提前触发了GC以获取足够的连续空间来“安置”它们。
    • 长期存活的对象将进入老年代。既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁。当它的年龄增加到一定程度(默认是15岁),将会被晋升到老年代。对象晋升到老年代的阈值可以通过参数-XX:MaxTenuringThreshold设置。

    老年代的空间不一定能容纳青年代所有存活的对象,一旦不能容纳,那就还需要进行一次Full GC。再谈MinorGC和FullGC的区别:
    Minor GC:从年轻代空间(包括Eden和Survivor区域)回收内存成为Minor GC。在发生Minor GC时候,有两处需要注意的地方:

    • 当JVM无法为一个新的对象分配空间时会触发Minor GC,例如当Eden区满了,所以分配的频率越高,执行Minor GC的频率也可能越频繁。
    • 所有的Minor GC都会触发“stop-the-world”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。

    Full GC:对整个堆进行整理,包括Young Generation、Old Generation、Permanent Generation。Full GC因为需要对整个区进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。

    总结:对象优先在Eden分配,大对象直接进入老年代,长期存活的对象将进入老年代,空间分配担保。

    《深入理解Java虚拟机》读书笔记——内存分配与回收策略

    7.简述重排序,内存屏障,happen-before,主内存,工作内存

    重排序:jvm虚拟机允许在不影响代码最终结果的情况下,可以乱序执行。

    内存屏障:可以阻挡编译器的优化,也可以阻挡处理器的优化

    happens-before原则:
    1:一个线程的A操作总是在B之前,那多线程的A操作肯定实在B之前。
    2:monitor 再加锁的情况下,持有锁的肯定先执行。
    3:volatile修饰的情况下,写先于读发生
    4:线程启动在一起之前 strat
    5:线程死亡在一切之后 end
    6:线程操作在一切线程中断之前
    7:一个对象构造函数的结束都该对象的finalizer的开始之前
    8:传递性,如果A肯定在B之前,B肯定在C之前,那A肯定是在C之前。

    主内存:所有线程共享的内存空间

    工作内存:每个线程特有的内存空间

    8.Java中存在内存泄漏问题吗?请举例说明

    内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。

    1.java中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。

    2.如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

    3.当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。

    《java内存泄漏场景》

    9.简述 Java 中软引用(SoftReferenc)、弱引用(WeakReference)和虚引用

    强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

    弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

    10.内存映射缓存区是什么
    11.jstack,jstat,jmap,jconsole怎么用?

    jps:查看所有的jvm进程,包括进程ID,进程启动的路径等等。
    jstack,观察jvm中当前所有线程的运行情况和线程当前状态。
    jstat,利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对进程的classloader,compiler,gc情况;特别的,一个极强的监视内存的工具,可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量,以及加载类的数量。
    jmap,监视进程运行中的jvm物理内存的占用情况,该进程内存内,所有对象的情况,例如产生了哪些对象,对象数量;系统崩溃了?jmap 可以从core文件或进程中获得内存的具体匹配情况,包括Heap size, Perm size等等
    jinfo,观察进程运行环境参数,包括Java System属性和JVM命令行参数。系统崩溃了jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息。

    《JVM性能调优监控工具jps、jstack、jstat、jmap、jinfo使用详解》

    12. 什么情况下会发生内存溢出?

    1.堆溢出
    创建对象时如果没有可以分配的堆内存,JVM就会抛出 OutOfMemoryError:java heap space异常
    2.栈溢出
    栈空间不足时,需要分下面两种情况处理:
    线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
    虚拟机在扩展栈深度时无法申请到足够的内存空间,将抛出OutOfMemberError
    3.永久代溢出
    永久代溢出可以分为两种情况,第一种是常量池溢出,第二种是方法区溢出。

    • 常量池溢出,要模拟常量池溢出,可以使用String对象的intern()方法。如果常量池包含一个此String对象的字符串,就返回代表这个字符串的String对象,否则将String对象包含的字符串添加到常量池中。

    《写代码实现堆溢出、栈溢出、永久代溢出、直接内存溢出》

    13.怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位
    14.JVM自身会维护缓存吗?是不是在堆中进行对象分配,操作系统的堆还是JVM自己管理堆
    15.32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?32 位和 64 位的 JVM,int 类型变量的长度是多数?
    16.双亲委派模型是什么

    相关文章

      网友评论

        本文标题:java深入篇之JVM

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