JVM 面试题

面试题及其解答

面试题题目汇总地址

图文并茂深入了解JVM

类元消息

然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
Class文件由类装载器装载后,元信息存放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象(元信息对象),用来封装类在方法区内的数据结构,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,这里就是我们经常能见到的Class类。

JVM内存区域及其存储的数据

在这里插入图片描述
JNI(Java Native Interface):本地库接口

Java虚拟机栈中存取的数据:基本数据类型、对象引用

关于基本数据类型的解释:线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,但它们之间是无法共享的。

在这里插入图片描述

堆:对象实例、数组
不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区

方法区:已被虚拟机加载的类元信息、常量、静态变量、运行时常量池

class文件

在这里插入图片描述

解释 Java 堆空间及 GC

当通过 Java 命令启动 Java 进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候,就从堆空间中分配内存。GC 是 JVM 内部的一个进程,回收无效对象的内存用于将来的分配。

说一下JVM的主要组成部分及其作用

1.类加载器(Class Loader):加载类文件到内存。Class loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是有Exectution Engine负责的。
2.执行引擎(Execution Engine) : 也叫解释器,负责解释命令,交由操作系统执行。
3.本地库接口(Native Interface)︰本地接口的作用是融合不同的语言为java所用
4.运行时数据区(Runtime Data Area) :
(1)堆。堆是java对象的存储区域,任何用new字段分配的java对象实例和数组,都被分配在堆上,java堆可用-Xms和-Xmx进行内存控制,jdk1.7以后,运行时常量池从方法区移到了堆上。
(2)方法区∶用于存储已被虚拟机加载的类信息(类名)、常量,静态变量,即时编译器编译后的代码等数据。常量池在方法区,误区:方法区不等于永生代
很多人原因把方法区称作永久代”(Permanent Generation),本质上两者并不等价,只是HotSpot虚拟机垃圾回收器团队把GC分代收集扩展到了方法区,或者说是用来永久代来实现方法区而已,这样能省去专门为方法区编写内存管理的代码,但是在Jdk8也移除了“永久代",使用Native Memory来实现方法区。
(3)虚拟机栈:虚拟机栈中执行每个方法的时候,都会创建一个栈桢用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
(4)本地方法栈:与虚拟机发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
(5)程序计数器。指示Java虚拟机下—条需要执行的字节码指令。
组件的作用:
首先通过类加载器(ClassLoader)会把 Java代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

本地库接口

深拷贝和浅拷贝

浅拷贝:同一个文件夹的两个快捷方式,虽然是两个不同的快捷方式,但是指向的文件夹是同一个,不管是通过哪个快捷方式进入,对该文件夹下的文件修改,相互影响。
深拷贝:我们复制某个文件夹(含里面的内容)在另外一个目录进行粘贴,就可得到具有相同内容的新目录,对新文件夹修改不影响原始文件夹

在这里插入图片描述

Java中堆和栈的区别

地址

总结:
1.功能和作用:堆主要用来存放对象的,栈主要是用来执行程序的
2:性能与存储要求:
在分配内存的时候,存放在栈中的数据大小与生存周期必须在编译时是确定的,缺乏灵活性。堆可以动态分配内存大小,编译器不必知道要从堆里分配多少存储空间,生存周期也不必事先告诉编译器,由于要在运行时动态分配内存和销毁对象时都需要占用时间,所以效率低,所以栈的速度比堆快,由于面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定
3.内存的分配:
Java中的数据类型有两种:一种是8个基本类型(即int, short, long, byte, float, double, boolean, char),一种是引用类型。函数中基本类型和对象的引用都是在栈内存中分配
对于引用类型:Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配
4.内存共享:
(1)栈数据的内存共享

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

String str3 = str1;

System.out.println(str3); //bcd

String str4 = "bcd";

System.out.println(str1 == str4); //true

假设我们同时定义:int a = 3; int b = 3;编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。
(2)堆中内存不共享

    String str1 = "abc";

    String str2 = new String("abc");

     System.out.println(str1==str2); //false

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。 以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

直接内存

和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
作为JAVA开发者我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。

优点:
1.减少了垃圾回收,因为垃圾回收会暂停其他的工作。
2.加快了复制的速度,堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作
缺点:
同样任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。
java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存

对象内存布局

在这里插入图片描述

地址

32 位和 64 位的 JVM,int 类型变量的长度是多数

理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。64 位 JVM允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
32 位和 64 位的 JVM 中,int 类型变量的长度是相同的,都是 32 位或者 4 个字节(一个字节8位)。(Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。)

怎样通过 Java 程序来判断 JVM 是 32 位 还是 64位

地址

JRE、JDK、JVM 及 JIT 之间有什么不同

JRE代表Java运行时(Java run-time),是运行Java引用所必须的。
JDK代表Java开发工具(Java development kit),是Java程序的开发工具,如Java编译器,它也包含JRE。
JVM代表Java 虚拟机 (Java virtual machine),它的责任是运行Java应用。
JIT代表即时编译(Just ln Time compilation),当代码执行的次数超过一定的阈值时,会将Java字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高Java 应用的性能。JIT这种功效很特殊,因为他把检测到的相似的字节码编译成单一运行的机器码,从而节省了CPU的使用。这和其他的字节码编译器不同,因为他是运行时(第一类执行的编译?)the firs of its kind to perform the compilation(从字节码到机器码)而不是在程序运行之前。正是因为这些,动态编译这个词汇才和JIT有那么紧密的关系。

内存泄漏和内存溢出

内存溢出:内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.

内存泄漏:内存泄露 memory leak,是指程序在申请内存后,可能会存在无用但可达的对象,无法释放已申请的内存空间,比如A和B互相引用,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因)、

联系:memory leak会最终会导致out of memory!

异常、堆内存溢出、OOM的几种情况

地址

简述java垃圾回收机制

在java中,不需要显示的去释放一个对象的内存,而是由虚拟机自动执行。在jvm中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行的,扫描哪些没有被任何引用的对象,并将它们添加到回收的集合中,进行回收。
垃圾收集的方式有:
1、标记-清除:标记哪些要被回收的对象,然后统一回收。有两个主要问题: a.效率不高,标记和清除的效率都很低; b.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
2、复制算法:为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次值使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。
3、标记-整理:该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可用回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
4、分代收集:根据对象的生命周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理或者标记-清除。

如何判断一个对象是否存活

判断一个对象是否存活有两种方法:引用计数法和可达性分析法

  • 引用计数法

    所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加1,引用失效时,计数器就减1。当一个对象的引用计数器为0时,说明此对象没有被其他对象引用,也就是死对象,将会被GC回收。
    缺陷:无法解决循环引用问题,也就是说对象A引用对象B,对象B反过来引用对象A,那么此时A、B对象的引用计数器都不为0,也就造成无法完成垃圾回收,就会造成内存泄漏,所以主流的虚拟机都没有采用这种算法。

  • 可达性分析法

    从一个被称为GC Roots的对象开始往下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
    可以作为GC Roots的对象有以下几种:

  • 虚拟机栈中引用的对象

  • 方法区类静态属性引用的对象 (静态属性属于全局变量,无法被回收)

  • 方法区常量池引用的对象

  • 本地方法栈引用的对象

    当一个对象不可达GC Roots时,这个对象并不会立马被回收,而是处于一个死缓的阶段,如果要真正的回收需要经历两次标记。如果对象在可达性分析中没有与GC Roots的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没有必要的。如果该对象有必要执行finalize()方法,那么这个对象就会放在一个称为F-Queue的队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()方法执行缓慢或者发生了死锁,那么就是造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于 F-Queue队列中的对象进行第二次标记,这时,该对象将会被移出“即将回收”集合,等待回收。

垃圾回收器的基本原理是什么?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。强制执行垃圾回收:System.gc()。Runtime.getRuntime().gc()
加粗样式

Java中都有哪些引用类型

类型一:强引用

强引用是一种最常见的引用形式,同时也较为普遍。如果内存空间不足,Java虚拟机将会抛出OutOfMemoryError错误,从而程序将异常停止。强引用的对象是不可以GC回收的,不可以随意回收具有强引用的对象来解决内存不足的问题。把一个对象赋给一个引用类型变量,则为强引用。在Java中,强引用是一种默认的状态,除非JVM虚拟机停止工作。
不可以随意回收具有强引用的对象来解决内存不足的问题

类型二:软引用

软引用和强引用不同,如果内存空间足够多,一个对象被软引用,则垃圾回收器不会将其回收;如果内存空间不足,这些引用对象就会被回收。所以,软引用就是当回收器没有回收某个对象时,程序就可以对其使用。它可用来较为敏感的高速缓存,虚拟机可以将软引用加入到与之向关联的队列。
如果内存空间不足,这些引用对象就会被回收

类型三:弱引用

弱引用的特点就是引用对象的生命周期较短。G回收器在扫描内存区域是若发现弱引用,即便是内存空间还足够使用,弱引用对象都会被回收。但弱引用对象也可以加入队列,这样就可以不被回收。
内存空间还足够使用,弱引用对象都会被回收

类型四:虚引用

从这种引用类型的名称就可以看出,虚引用的对象可以说是形同虚设。为什么这么说呢?因为虚引用不会决定对象的生命周期,并且虚引用等于没有引用,随时都可以被GC回收。
因为虚引用不会决定对象的生命周期,并且虚引用等于没有引用,随时都可以被GC回收。

JVM 运行时堆内存如何分代

在这里插入图片描述

一般情况下,新创建的对象都会分配到Eden区,一些特殊的大的对象会直接分配到old区
比如有对象A,B,C等创建Eden区,但是Eden区的内存空间肯定有限,比如有10OM,假如已经使用了100M或者到达一个设定的临界值,这时候就需要对Eden内存空间进行清理,即垃圾收集,这样的GC我们称之为Minor GC ,Minor GC指得是Young区的GC
经过GC之后,有些对象就会被清理掉,有些对象可能还在存活着,对于存活的对象需要将其复制到Survivor区,然后再清空Eden区中的这些对象
Survivor区详解
survivor区分为so和s1,也可以叫From to在同一个时间点上,so和s1只能有一个区有数据,另一个是空的
接着上面的GC来说,比如一开始只有Eden区和From中有对象,To中是空的。此时进行一次GC操作,From区中对象的年龄就会+1,我们知道Eden区中所有存活的对象会被复制到To区,
From区中还能存活的对象会有两个去处。若对象年龄达到之前设置好的年龄阈值,此时对象会被移动到Old区,如果Eden区和From区没有达到阈值的对象会被复制到To区。
此时Eden区和From区已经被清空(被GC的对象肯定没了,没有被GC的对象都有了各自的去处)。这时候From和To交换角色,之前的From变成了To,之前的To变成了From。
也就是说无论如何都要保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,知道To区被填满,然后会将所有对象复制到老年代中。

为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,并且没有年龄限制的话,存活的对象就会被送到老年代。这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。执行时间长有什么坏处?频发的FullGC消耗的时间很长,会影响大型程序的执行和响应速度。
可能你会说,那就对老年代的空间进行增加或者较少咯。假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full
GC,执行所需要的时间更长。假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。

对象晋升到老年代一共有四种情况

  1. 对象太大,Eden放不下
  2. 存放存活对象的Survivor区太小,不足以存下存活对象
  3. 经历超过默认15次gc或者设定的
  4. Survivor空间中相同年龄的所有对象总和大于等于Survivor空间的一半,那么这些对象就会直接进入到老年代中

jvm的永久代会发生垃圾垃圾回收吗

在这里插入图片描述

JAVA8与元数据

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间 的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用 本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize控制, 而由系统的实际可用空间来控制

Minor GC ,Major GC,Full GC 触发条件

Minor GC:新生代

Major GC:Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他指的时full GC还是old gen。
Full GC:新生代+老年代

Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式。Old GC执行时,一般都会带上一次Y-GC,很多JVM实现里他触发的实际上就是Full GC,其实满足上述一些条件时,在GC日志中看到的就是Full GC字样。

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

Full GC触发条件:

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

(2)老年代空间不足

(3)方法区空间不足

(4)老年代可用连续内存空间(新Survivor空间中相同年龄的所有对象总和大于等于Survivor空间的一半,那么这些对象就会直接进入到老年代中,如果本次Y-GC后,可能升入老年代的对象大小超过老年代当前可用内存空间,此时必须先触发一次Old GC给老年代腾出空间)

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

如何优化GC

1.尽量不要创建过大的对象或数组。

2.通过虚拟机的 -Xmn 参数适当调大新生代的大小,让对象尽量在新生代中被回收掉。

3.通过 -XX:MaxTenuringThreshold(老年化阈值) 参数调大对象进入老年代的年龄,让对象尽量在新生代中被回收掉。

JVM有哪些垃圾回收算法

地址

JVM垃圾收集器

在这里插入图片描述

图3-6展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器

在垃圾收集器中并行与并发的概念

在这里插入图片描述

通俗理解记忆集和卡表

地址

Serial(串行的)收集器

在这里插入图片描述

Serial收集器只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的"单线程"的意义并不仅仅是说明它只使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其它所有的线程,直到它收集结束,即“Stop the world”。
缺点:在用户不可知的、不可控的情况下把用户的正常工作的线程全部停掉,会极大的影响用户的体验
优点:Serial收集器依然是HotSpot虚拟机在客户端模式下的默认新生代收集器,与其他收集器的单线程相比,它的优点那就是简单而高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗( Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在非常短的时间内(十几、几十毫秒,最多一百多毫秒以内),只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择

ParNew收集器

在这里插入图片描述

ParNew收集器本质上是Serial收集器的多线程并发版本,除了同时使用多条线程进行垃圾收集之外,其余的行为与Serial收集器完全一致
优点:它在JDK7之前的遗留系统中首选的新生代收集器。因为除了Serial收集器外,只有它能与CMS收集器配合工作,自JDK 9开始,ParNew和CMS从此只能互相搭配使用,再也没有其它收集器能够和它们配合了

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记–复制算法实现的收集器,也是能够并行收集的多线程收集器。
特点:Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB 快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

在这里插入图片描述

Serial Old收集器

Serial Old是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记–整理算法,这个收集器的主要意义也是供客户端模式下的HotSpot 虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用°,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记–整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的 Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了ParallelScavenge 收集器,老年代除了Serial Old ( PS MarkSweep〉收集器以外别无选择,但是单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力。
直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
在这里插入图片描述

CMS 收集器

在这里插入图片描述

CMS ( Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。在较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验的应用上,CMS收集器就非常符合这类应用的需求。从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,整个过程分为四个步骤。
(1)初始标记:初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
(2)并发标记:并发标记阶段就是从GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
(3)重新标记:而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
(4)并发清除:最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记为什么需要STW?
因为初始标记标记的是GC Root,而GC Root容易变动,比如栈帧中的本地变量表。所以需要STW

优点:并发收集、低停顿
主要缺点:
(1)CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。
解决方案:是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显
(2)CMS 收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failurc”失败进而导致另一次完全“Stop The World”的Full GC的产生。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”( Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old 收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
(3)CMS是一款基于“标记–清除”算法实现的收集器,=这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。设置参数的作用要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理。

Garbage First (G1)收集器

在这里插入图片描述

G1是一款主要面向服务端应用的垃圾收集器,作为CMS收集器的替代者和继承人,HotSpot的开发者们希望做出一款能够建立起“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然Gl也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,Gl的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1回收垃圾的思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。

G1收集器的运作过程大致可划分为以下四个步骤:
(1)初始标记:仅仅只是标记一下 GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region中分配新对象。这个阶段需要停顿线程,但耗时很短。
(2):并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
(3)最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
(4)筛选回收:负责更新Region的统计数据,对各个 Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region 中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的

优点:
(1)从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。
(2)毫无疑问,可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
(3):从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路从工程实现上看是从G1开始兴起的,所以说Gl是收集器技术发展的一个里程碑。
(4)与CMS的“标记–清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。

缺点:就内存占用来说,虽然G1和 CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(这种结构不仅记录"我指向谁",还记录了"谁指向我")可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。

符号引用和直接引用

地址

java类加载机制

详解地址
在这里插入图片描述

浓缩版:
加载

类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,然后将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

验证

目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
准备

为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。
解析

这里主要的任务是把常量池中的符号引用替换成直接引用(在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language来表示Language类的地址)
初始化

这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)

总结:可以通俗理解为先找到一个类的完全限定名查找此类找到该类的.class文件,然后读入内存,创建出class对象,然后class文件的字节流中包含信息验证其安全性和合理性,然后再初始化static修饰的字段变量为默认值,然后再导入依赖包,然后再执行初始化

双亲委派机制

地址

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器在它的加载路径下没有找到所需加载的Class,子类加载器才会尝试在自己去加载。

可以打破双亲委派么,怎么打破?

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。
如果想自定义类加载器,就需要继承ClassLoader,并重写findClass,如果想不遵循双亲委派的类加载顺序,还需要重写loadClass。

什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

在这里插入图片描述

JVM调优

地址

JVM常用命令

JVM堆空间大小的设置(-Xms和-Xmx)和查看

通过虚拟机的 -Xmn(mn:memory new generation ) 参数适当调大新生代的大小,让对象尽量在新生代中被回收掉。
通过 -XX:MaxTenuringThreshold(老年化阈值) 参数调大对象进入老年代的年龄,让对象尽量在新生代中被回收掉。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值