面试整理-JAVA部分-JVM

目录

1、Java部分

1.1  java基础

1.2 JVM学习笔记

1、内存模型以及分区,需要详细介绍到每个分区存放什么?

2、堆里面的分区:Eden、survival from to和老年代,各自的特点。

3、对象创建方法,对象的内存分配,对象的访问定位。

4、GC的两种判定方式:引用计数与引用链。

5、GC的三种收集方法:标记清除、标记整理和复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路。

6、GC收集器有哪些?CMS收集器与G1收集器的特点。

7、Minor GC与FULL GC分别发生在什么地方?

8、类加载的五个过程:加载、验证、准备、解析和初始化。

9、双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。

10、分派:静态分派与动态分派。

11、JVM性能调优。


1、Java部分

1.1  java基础

1.2 JVM学习笔记

1、内存模型以及分区,需要详细介绍到每个分区存放什么?

JVM初始运行的时候都会分配好Method Area(方法区)和Heap(堆),而JVM每创建一个线程,就为其分配一个Program Counter Register(程序计数器),VM Stack(虚拟机栈)和Native Method Stack(本地方法栈),当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与JAVA程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说发生在Heap上)的原因。

  1. 线程私有区

程序计数器:一块较小的内存空间,存放下一条即将执行指令的地址。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。每一个线程都有一个程序计数器(内存),这样线程切换的时候就能找到自己各个线程各自即将执行的下一条指令。唯一一个没有OutOfMemoryError异常的区域。

虚拟机栈:每个方法在执行时都会创建一个栈帧,用户存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 对这个区域定义了两种异常状态 OutOfMemoryError StackOverflowError,当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。

局部变量表:存放方法参数和方法内的局部变量。(存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。)

本地方法栈:虚拟机的Native方法执行的内存区。与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。(使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。 这些函数的实现体在DLL中,JDK的源代码中并不包含,你应该是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。)

  1. 线程共享区

堆:存放了对象实例及数组(所有new的对象)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。

新生代:

程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。

老年代:

用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,且数组中无引用外部对象。

老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。

定义了OutOfMemoreyError异常(堆中没有足够内存完成实例分配时)。

方法区(永久代,非堆):存放类信息、常量、静态变量、编译器编译后的代码等数据(GC在这个区域很少出现,这个区域内存回收的目标主要是对常量池的回收和类型的卸载,回收的内存比较少,所以也有称这个区域为永久代(Permanent Generation)的。当方法区无法满足内存分配时抛出OutOfMemoneyError异常,默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC))

运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。

2、堆里面的分区:Eden、survival from to和老年代,各自的特点。

Eden以及Survivor共同组成New Generatiton/Young space。通常将对New Generation进行的回收称为Minor GC;对Old Generation进行的回收称为Major GC,但由于Major GC除并发GC外均需对整个堆以及方法区进行扫描和回收,因此又称为Full GC。

1.Eden区

Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。

2.Survival from to

Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survival from区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。

3.年老代

年老代里存放的都是存活时间较久的,大小较大的对象。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

3、对象创建方法,对象的内存分配,对象的访问定位。

  • Java对象的创建大致上有以下几个步骤:
  1. 类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程
  2. 为对象分配内存:对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。由于堆被线程共享,因此此过程需要进行同步处理(分配在TLAB上不需要同步)
  3. 内存空间初始化:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 对象设置:JVM对对象头进行必要的设置,保存一些对象的信息(指明是哪个类的实例,哈希码,GC年龄等)
  5. init:执行完上面的4个步骤后,对JVM来说对象已经创建完毕了,但对于Java程序来说,我们还需要对对象进行一些必要的初始化。
  • 对象的内存分配:

Java对象的内存分配有两种情况,由Java堆是否规整来决定(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定):

  1. 指针碰撞(Bump the pointer):如果Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离
  2. 空闲列表(Free List):如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
  • 对象的访问定位:

对象的访问形式取决于虚拟机的实现,目前主流的访问方式有使用句柄和直接指针两种:

使用句柄:

如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针:

如果使用直接指针访问对象,那么对象的实例数据中就包含一个指向对象类型数据的指针,引用中存的直接就是对象的地址。

优势:速度更快,节省了一次指针定位的时间开销,积少成多的效应非常可观。

4、GC的两种判定方式:引用计数与引用链。

基于引用计数与基于引用链这两大类别的自动内存管理方式最大的不同之处在于:前者只需要局部信息,而后者需要全局信息。

引用计数:

记录一个对象被引用指向的次数。引用计数方式最基本的形态就是让每个被管理的对象与一个引用计数器关联在一起,该计数器记录着该对象当前被引用的次数,每当创建一个新的引用指向该对象时其计数器就加1,每当指向该对象的引用失效时计数器就减1。当该计数器的值降到0就认为对象死亡。每个计数器只记录了其对应对象的局部信息——被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象;但也因为缺乏全局对象图信息,所以无法处理循环引用的状况。

引用链:

引用链需要内存的全局信息,当使用引用链进行GC时,从对象图的“根”(GC Root,必然是活的引用,包括栈中的引用,类静态属性的引用,常量的引用,JNI的引用等)出发扫描出去,基于引用的可到达性算法来判断对象的生死。这使得对象的生死状态能批量的被识别出来,然后批量释放死对象。引用链不需要显式维护对象的引用计数,只在GC使用可达性算法遍历全局信息的时候判断对象是否被引用,是否存活。

5、GC的三种收集方法:标记清除、标记整理和复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路。

  • 标记清除:

标记清除算法分两步执行:

  1. 暂停用户线程,通过GC Root使用可达性算法标记存活对象。
  2. 清除未被标记的垃圾对象。

      标记清除算法的缺点如下:

  1. 效率较低,需要暂停用户线程。
  2. 清除垃圾对象后内存空间不连续,存在较多内存碎片。

标记算法如今使用不多。

  • 复制算法(from和to):

复制算法也分两步执行,在复制算法中一般会有至少两片的内存空间(一片是活动空间,里面含有各种对象,另一片是空闲空间,里面是空的):

  1. 暂停用户线程,标记活动空间的存活对象
  2. 把活动空间的存活对象复制到空闲空间去,清除活动空间

复制算法相比标记清除算法,优势在于其垃圾回收后的内存是连续的。

但是复制算法的缺点也很明显:

  1. 需要浪费一定的内存作为空闲空间
  2. 如果对象的存活率很高,则需要复制大量存活对象,导致效率低下

复制算法一般用于年轻代的Minor GC,主要是因为年轻代的大部分对象存活率都较低。

  • 标记整理:

算法不直接对可回收对象进行清理,而是让所有可用的对象都向一端移动。然后直接清理掉边界以外的内存。

标记整理算法是标记清除算法的改进,分为标记、整理两步:

  1. 暂停用户线程,标记所有存活对象
  2. 移动所有存活对象,按内存地址次序依次排列,回收末端对象以后的内存空间。

标记整理算法与标记清除算法相比,整理出的内存是连续的;而与复制算法相比,不需要多片内存空间。

然而标记整理算法的第二步整理过程较为麻烦,需要整理存活对象的引用地址,理论上来说效率要低于复制算法。因此标记整理算法一般引用于老年代的Major GC。

6、GC收集器有哪些?CMS收集器与G1收集器的特点。

此处主要介绍CMS和G1。

1、CMS收集器(Concurrent Mark Sweep,并发标记清除收集器)

CMS收集器是一个年老代的收集器,是以最短回收停顿时间为目标的收集器。

CMS收集器基于标记清除算法实现,主要分为4个步骤:

  • 初始标记

在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。

  • 并发标记

这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

  • 并发预清理

并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。

  • 重新标记

这个阶段会需要stop the world,收集器线程扫描在CMS堆中剩余的对象。扫描从"GC Root"开始向下追溯,并处理对象关联。

  • 并发清理

清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

  • 并发重置

这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS收集器的缺点在于:

  1. 其对于CPU资源很敏感。在并发阶段,虽然CMS收集器不会暂停用户线程,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量降低。其默认启动的回收线程数是(cpu数量+3)/4,当cpu数较少的时候,会分掉大部分的cpu去执行收集器线程。
  2. 无法处理浮动垃圾,浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集。
  3. CMS收集器使用的是标记清除算法,GC后会产生碎片。

2、G1收集器(Garbage First收集器

相比CMS收集器,G1收集器主要有两处改进:

  1. 使用标记整理算法,确保GC后不会产生内存碎片
  2. 可以精确控制停顿,允许指定消耗在垃圾回收上的时间

G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

7、Minor GC与FULL GC分别发生在什么地方?

Minor GC也叫Young GC,当年轻代内存满的时候会触发,会对年轻代进行GC。

Full GC也叫Major GC,当年老代满的时候会触发,当我们调用System.gc时也可能会触发,会对年轻代和年老代进行GC。

8、类加载的五个过程:加载、验证、准备、解析和初始化。

JVM把class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成JVM可以直接使用的Java类型的过程就是加载机制。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称连接。

  • 加载

在加载阶段,虚拟机需要完成以下事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构;
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问接口。
  • 连接

    (1)验证

在验证阶段,虚拟机主要完成:

  1. 文件格式验证:验证class文件格式规范。
  2. 元数据验证:这个阶段对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范要求。
  3. 字节码验证:进行数据流和控制流分析,这个阶段对类的方法进行校验分析,这个阶段的任务是保证被校验的类的方法在运行时不会做出危害虚拟机的安全的行为。
  4. 符号引用验证:符号引用中通过字符串描述的全限定名是否能够找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否被当前类访问。

     (2)准备

准备阶段是正式为类变量分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。

     (3)解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,常见的解析有四种:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

    (4)初始化

初始化阶段才真正开始执行类中定义的java程序代码,初始化阶段是执行类构造器(clinit)()方法的过程。

9、双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader。

 

启动(Bootstrap)类加载器:

负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

标准扩展(Extension)类加载器:

负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

应用程序(Application)类加载器:

负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

除此之外,还有自定义的类加载器,它们之间的层次关系被称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

双亲委托的工作过程:

如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载。

使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object存放在rt.jar之中,无论那个类加载器要加载这个类,最终都是委托给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,相反,如果没有双亲委托模型,由各个类加载器去完成的话,如果用户自己写一个名为java.lang.Object的类,并放在classpath中,应用程序中可能会出现多个不同的Object类,java类型体系中最基本安全行为也就无法保证。

10、分派:静态分派与动态分派。

引用类型:静态类型。

静态分派:

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(重载是通过参数的静态类型而不是实际类型来选择重载的版本的)

动态分配:

与静态分派类似,动态分派指在在运行期根据实际类型确定方法执行版本,其典型应用是方法重写(即多态)。

可以看出来重写是一个根据实际类型决定方法版本的动态分派过程。

11、JVM性能调优。

1、性能定义

吞吐量:重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。

延迟:其度量标准是缩短由于垃圾收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。

内存占用:垃圾收集器流畅运行所需要的内存数量。

这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。

2、性能调优原则

MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。

GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。

GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。

3、性能调试

  • 年轻代大小选择

(1)响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

(2)吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

  • 年老代大小选择

     (1)响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

  1. 并发垃圾收集信息
  2. 持久代并发收集次数
  3. 传统GC信息
  4. 花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

      (2)吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

  • 较小堆引用的碎片问题

因为年老代(CMS)的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

  1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
  2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

猜你喜欢

转载自blog.csdn.net/u013094043/article/details/82822582