1 组成
1、JVM 是由哪几部分组成的?
JVM(Java virtual machine)是 Java 程序的运行环境,它同时也是一个操作系统的一个应用程序,因此 JVM 也有他自己的运行生命周期,也有自己的代码和数据空间。
JVM 主要由两个子系统以及两个组件r 组成:
Class loader(类加载):根据给定的全限定类名将字节码 class 文件装载到运行时数据区域的 method area(方法区)中;
Execution engine(执行引擎):执行 class 中的指令;
Runtime data area(运行时数据区):这就是我们常说的 JVM 内存,也就是堆区和栈区;
Native interface(本地接口):与 native libraries 交互,也是与其它编程语言的交互接口。
2、说一下运行时数据区的组成?
JVM 在执行 Java 程序时,会把其所管理的内存划分成多个区域,每个区域都有不同用途,每个区域的创建和销毁时间也不同。
由于各大厂家虚拟机的实现各个区域划分可能有所不同,但 JVM 规范中规定的区域可以分为以下几个部分:
程序计数器(Program Counter Register)
是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。在虚拟机概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此未来线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,故程序计数器是线程私有的。
如果线程正在执行的是一个 Java 方法,这个计数器记录的则是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器则为空(undefined)。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈(VM Stack)
和程序计数器一样,虚拟机栈也是线程私有的,且其生命周期与线程相同。
在栈中,每执行一个方法前都会创建一个栈帧,来储存局部变量表、操作数栈、动态链接以及方法出口等信息。每个方法从调用到完成,就对应着栈中一个栈帧的压栈到弹出的过程。
同时一个方法对应的栈帧中局部变量表所需内存空间早已在编译期间就分配完成,当进入一个方法,虚拟机栈对应压入一个栈帧,次栈帧局部变量表空间大小完全确定。
在 Java 虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出 Stack OverflowError 异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
本地方法栈(Native Method Stacks)
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
但在我们最常用的 HotSpot 虚拟机中,本地方法栈与 Java 虚拟机栈合二为一
堆(Heap)
Java 堆是一块线程共享的内存区域,它在虚拟机启动时分配创建,它是 JVM 内存里最大的一块内存,但是它的目的只有一个:存放对象实例。
但是随着 JIT 编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也就变得不那么绝对了。
方法区(Method Area)
又被称非堆(Non-Heap),JVM 虚拟机规范中把方法区描述为堆的一个逻辑部分,它与 Java 堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。但这部分区域又比较特殊,且虚拟机如何实现方法区不受虚拟机规范约束。比如 HotSpot 中,JDK 1.7 之前方法区为永久代,其垃圾回收受 Java 堆 GC 分代收集算法管理,1.7 将 String 常量池移入堆中,1.8 中永久代被完全移除,将类加载信息放入一个线程共享的元空间内。
直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,也不是 JVM 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 出现。
如 NIO 使用 Native 函数库直接分配堆外内存,然后通过一个 DirectByteBuffer 对象作为这块内存的引用,可以大幅度减轻堆的负担,提高虚拟机性能。
总结一下:
1)JVM 的内存模型中一共有两个“栈”,分别是:虚拟机栈和本地方法栈。两个“栈”的功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都是线程私有。只不过虚拟机栈描述的是 Java 方法运行过程的内存模型;而本地方法栈是描述 Java 本地(Native)方法运行过程的内存模型。
2)JVM 的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。方法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译的代码。
3)堆是 JVM 中最大的一块内存区域,也是垃圾收集器主要的工作区域。
4)程序计数器、虚拟机栈、本地方法栈是线程私有的。并且它们的生命周期和所属的线程一样。而堆、方法区是线程共享的,在 JVM 中只有一个堆、一个方法栈。并且在 JVM 启动的时候就创建,JVM 停止才销毁。
3、虚拟机栈帧的组成?
局部变量表(Local Variable Table)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译 Class 文件时就在方法的 code 属性 max_locals 中确定了该方法所需要分配的局部变量表的最大容量。
操作数栈(Operand Stack)
也常称为操作栈,它是一个后入先出(Last In First out,LIFO)栈。
动态连接(Dynamic Linking)
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
4、什么是元空间?
JDK 1.8 以前的 HotSpot 有个叫方法区的内存区域,也叫永久代(permanent generation)。而从 JDK 1.7 开始,方法区的部分数据就被移除:符号引用(Symbols)移至 Native heap,字面量(interned strings)和静态变量(class statics)移至 Java heap。
至于为什么要用**元空间(Metaspace)**替代方法区,这是因为随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
2 内存
1、堆和栈的区别?
物理地址:堆不连续,性能慢;
内存分配:堆分配的内存是在运行期确认,栈在编译器,大小固定;
存放内容:对象、数组 vs 局部变量,操作数栈,返回结果;
可见性:堆对于整个程序共享可见,栈是线程私有,生命周期同线程。
2、对象是如何创建的?
1)类加载
当 JVM 遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析以及初始化。如果没有,则先执行类加载过程。
(此过程详见类加载章节)
2)内存分配
类加载检查通过后,对象所需的内存空间大小在类加载完成之后便可确定,JVM 会根据垃圾回收期选取内存分配算法:
指针碰撞法
serial,ParNew 等带有压缩功能的回收器,内存是连续的,内存指针移动基于对象大小移动。
空闲列表法
CMS,通过维护一个空间内存列表,存放对象、分配内存还需考虑并发申请内存问题(CAS、TLAB本地线程缓冲)。
堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错。虚拟机必须维护一个列表,记录那些内存块是可用的,在分配的时候从列表中找到一块足够大的内存划分给对象实例,并更新列表上的记录。
3)初始化
内存分配完成之后,则需要初始化对象的信息,主要涉及属性默认值、对象头信息以及执行构造函数。
JVM 就需要将分配的内存空间都初始化为零(不包括对象头),如果在 TLAB 上分配内存,此过程可提前至 TLAB 分配时进行。这一步保证了对象的实例字段可以不赋初值也可以直接使用。
设置对象头信息,这些信息包括该对象是那个类的实例,如何才能找到该类的元数据信息,对象的哈希码,对象的 GC 分代信息等。
执行完以上步骤之后,对于 JVM 来说新的对象已经创建完成,但对于 Java 程序来说,对象创建才刚开始,因为构造函数还没有执行,所以要执行方法进行自定义初始化。
3、创建对象如何解决并发问题?
由于 JVM 中创建对象的行为非常频繁,因此需要考虑内存分配的并发问题解决方案:
1)对分配内存空间的动作进行同步,即用CAS失败重试的方式;
2)把内存分配的动作按照线程划分在不同的空间中进行,每个线程在 Java 堆中预先分配一小块内存,即本地线程分配缓冲 TLAB(Thread Local Allocation Buffer),各线程首先在 TLAB 上分配内存,TLAB 使用完之后,分配新的 TLAB 时才需要同步锁定。JVM 是否使用 TLAB 可以通过 -XX:+/-UseTLAB 参数指定。
4、如何定位到内存中的对象?
Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。由于 reference 类型在 JVM 规范中只规定了一个指向对象的引用,并未定义这个引用如何定位和访问具体位置,所以对象访问方式由具体虚拟机实现而定。目前主流的访问方式有句柄和直接指针两种。
直接指针:
ava堆对象的布局中就必须考虑如何放置访问方法区中类型数据的相关信息。
句柄访问
Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例和类型数据各自的具体地址信息。
二者各有优势,使用句柄访问这样做的好处是栈中 reference 存储的句柄地址较为稳定,因为在 Java 堆中进行了垃圾回收,对象的地址发生了改变的时候,只需要修改句柄的对象实例数据指针就行。而使用直接指针的最大好处就是速度更快。
5、对象在内存中是怎么存在的?
对象在堆内存的内存布局主要有三部分,即对象头、实例数据以及对其填充。
1)对象头(Header)
对象头主要包含两部分的内容,一个叫做运行时元数据(MarkWord),一个叫做类型指针(Class Metadata Address)。
类型指针指向元数据区代表当前类的 class 对象,确定该对象所属的类型,而运行时元数据又包含了:
哈希值(hashcode),也就是对象在堆空间中都有一个首地址值,栈空间的引用根据这个地址指向堆中的对象,这就是哈希值起的作用;
GC 分代年龄:对象首先是在Eden中创建的,在经过多次GC后,如果没有被进行回收,就会在 survivor 中来回移动,其对应的年龄计数器会发生变化,达到阈值后会进入养老区;
锁状态标志:在同步中判断该对象是否是锁;
线程持有的锁;
线程偏向ID;
偏向时间戳。
2)实例数据(Instance Data)
它是对象真正存储的有效信息,包括程序代码中定义的各种字段类型,当然也包含从父类继承下来的字段。注意这里有一些规则:相同宽度的字段总是被分配在一起,父类中定义的变量会出现在子类之前,因为父类的加载是优先于子类加载的。
3)对齐填充
没有特殊含义,仅仅起到占位符的作用。
6、内存溢出与内存泄漏问题?
3 垃圾回收
1、什么是垃圾回收系统?
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,对程序而言它们已经死亡),为了确保程序运行时的性能,Java 虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
JVM 的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点。而缺点也显而易见,Java 并未提供显式的内存管理操作,仅有的 System.gc() 方法也只是通知 JVM 需要进行 GC 操作,但是否执行仍需 JVM 决定。
2、哪些内存区域需要回收?
程序计数器、虚拟机栈、本地方法栈三个区域随线程而生随线程而灭。虚拟机栈中的栈帧随着方法的开始和结束对应着入栈和出栈。每一个栈帧需分配内存的大小在类结构确定下来时就已知了。因此这几个区域的内存分配和回收都具有确定性。方法结束时或是线程结束时内存会随之回收。
在 JVM 的堆和方法区中,一个接口的实现类所需的内存可能不同,一个方法的不同分支所需内存也可能不同。只有在程序运行期才能知道要创建多少对象,这部分的内存分配和回收具有动态性。
3、如何判断对象是否可以回收?
Java 在垃圾回收之前,需要判断对象是否存活,只有死亡的对象才能被 GC 回收。常用的两种方式是:引用计数法和可达性分析。
引用计数
给对象添加一个引用计数器,每当有一个对象引用时 +1,当引用失效时 -1,任何时刻计数器为 0 的对象就是可以被回收的对象。
引用计数发实现虽然简单,且很高效,但很难解决对象之间循环引用的问题。
可达性分析
通过一系列称为 GC Roots 的对象作为起始点,从这些点向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则认为对象不可达。
即使在可达性分析中不可达的对象,也并非是非死不可,只是处于缓刑阶段,要真正死亡至少要经历两次标记,这跟 finalize 有关。
4、GC Roots 对象有哪些?
1)虚拟机栈中引用的对象;
2)方法区中(1.8称为元空间)的类静态属性引用的对象;
3)方法区中的常量引用的对象;
4)本地方法栈中的JNI(native方法)引用的对象。
5、对象的回收过程?
JVM 在对 Java 对象回收阶段进行两次标记,一次筛选。
1)如果可达性分析算法发现对象没有在 GC ROOT 引用链中,进行第一次标记并筛选是否需要调用重写的 finalize() 方法。
2)筛选的依据是被回收对象是否重写过 finalize() 方法,且该重写方法并未被虚拟机调用过,不满足直接进行回收。
3)如果该对象有必要执行 finalize() 方法,该对象就会被放置在一个叫 F-Queue 的队列之中 ,并在稍后会由虚拟机自动创建一个低优先级的线程 Finalizer 线程去执行它。这个线程只会触发这个方法,不一定会等它结束,原因是防止一个 finalize() 方法在虚拟机中执行缓慢或死循环,导致 F-Queue 队列中其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将会对 F-Queue 队列中的对象进行第二次小规模的标记,如果对象在 finalize() 方法中重新与引用链上的任何一个对象建立联系,就可以拯救自己。
6、方法区能否被回收?
有些人认为方法区(如 HotSpot 中的元空间或者永久代)是没有垃圾收集行为的,《Java虚 拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11 时期的 ZGC 收集器就不支持类卸载),方法区垃圾收集的性价比通常也是比较低的,在 Java 堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收 70-99% 的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。
方法区的垃圾收集主要回收两部分,废弃的常量和不再使用的类型。
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类及其任何派生子类的实例。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP 的重加载等,否则通常是很难达成的。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
JVM 被允许对满足上述三个条件的无用类进行回收,而并不是和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 还提供了 -Xnoclassgc 参数进行控制。
7、四种引用类型?
强引用:永不回收,Java 中默认引用;
软引用:内存溢出前回收;
弱引用:下次GC时回收;
虚引用:无法通过 PhantomReference 获取对象,作用是 GC 时返回一个通知。
8、有哪些垃圾回收算法?
由于 Java 虚拟机规范中并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。
常见的算法有新生代的复制算法,以及老年代的标记清除和标记整理算法。
Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,因为它最容易实现,思想也是最简单的。标记-清除算法分为标记和清除两个阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
Copying(复制)算法
为了解决标记清除算法的缺陷,复制算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。同时回收效率也和存活对象数量有着极大关系。
Mark-Compact(标记-整理)算法
为了解决复制算法的缺陷,充分利用内存空间,提出了标记整理算法,也称压缩算法。该算法标记阶段和标记清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照 1:1 的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)。
9、新生代为划分空间的比例?
通常将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,其比例是 8:1:1。这是一个根据统计学得出的数据,新生代对象生存时间比较短,大约 80% 对象被回收。
10、常见的垃圾回收器?
图中展示了 7 种作用于不同分代的收集器,分别是新生代收集器 Serial、ParNew、Parallel Scavenge,老年代收集器 CMS、Serial Old、Parallel Old 以及整堆收集器 G1。如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
Serial 收集器
单线程、简单高效,对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程回收效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
ParNew 收集器
ParNew 收集器其实就是Serial收集器的多线程版本。除了使用多线程外其余行为均和 Serial 收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等),同样存在 STW 问题。
可以使用 -XX:ParallelGCThreads 参数来设置垃圾收集的线程数。许多运行在 Server 模式下的虚拟机中首选的新生代收集器,因为它是除了 Serial 外,唯一能与 CMS 收集器配合工作的。
Parallel Scavenge 收集器
这是一个可以控制吞吐量的多线程收集器,故也称为吞吐量优先收集器。相比 ParNew 收集器,Parallel Scavenge 可以使用 XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间以及 XX:GCRatio 直接设置吞吐量的大小。
同时支持 GC 自适应调节策略。即 Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。当打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等信息,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量。
Serial Old 收集器
Serial 收集器的老年代版本,同样是单线程收集器,采用标记-整理算法。
Parallel Old 收集器
是 Parallel Scavenge收集器的老年代版本,采用标记-整理算法。适用于注重高吞吐量以及 CPU 资源敏感的场合。
CMS 收集器
一种以获取最短回收停顿时间为目标的收集器。也是基于标记-清除算法的并发收集器。此处并发同步于前面提及的几种多线程并行收集器,是指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。
非常适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景。
G1 收集器
一款面向服务端应用的垃圾收集器。
11、CMS 是如何工作的?
CMS 收集器的运行过程分为下列 4 步:
1)初始标记阶段,标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
2)并发标记阶段,进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
3)重新标记阶段,为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,也存在 STW 问题。
4)并发清除阶段,对标记的对象进行清除回收。
CMS 收集器的缺点:
对 CPU 资源非常敏感。
无法处理浮动垃圾,可能出现 Concurrent Model Failure 失败而导致另一次 Full GC 的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次 Full GC。
12、G1 收集器是如何工作的?
如果不计算维护 Remembered Set 的操作,G1 收集器大致可分为如下步骤:
1)初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
2)并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
3)最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
4)筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
13、触发 GC 的条件?
先来了解一下什么是 MinorGC(Young GC) 和 Major GC/FullGC。
1)MinorGC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为 Java 对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。
MinorGC 触发条件也很简单:当年轻代空间不足时,就会触发 MinorGC,注意这里的年轻代满指的是 Eden 区,Survivor 满不会引发 MinorGC。
同时 Minor GC 会引发 STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
2)Major GC
CMS 收集器中,当老年代满时会触发 Major GC。且目前只有 CMS 收集器会有单独收集老年代的行为。
3)Full GC
Full GC 对收集整堆(新生代、老年代)和方法区的垃圾收集。
当年老代满时会引发 Full GC,将会同时回收新生代、年老代 ;当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载。
调用System.gc时,系统建议执行Full GC,但是不一定会执行;
老年代空间不足;
方法区空间不足;
通过 Minor GC 后进入老年代的空间大于老年代的可用内存;
由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
14、什么是三色标记?
在垃圾回收算法中,标记总是必可少的一步。要找出存活对象,根据可达性分析,从 GC Roots 开始进行遍历访问,同时我们把遍历对象图过程中遇到的对象,按是否访问过这个条件标记成以下三种颜色:
白色:尚未访问过;
黑色:本对象已访问过,而且本对象引用到的其他对象也全部访问过了;
灰色:本对象已访问过,但是本对象引用到的其他对象尚未全部访问完。待全部访问后,会转换为黑色。
假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:
1)初始时,所有对象都在白色集合中;
2)将 GC Roots 直接引用到的对象挪到灰色集合中;
3)从灰色集合中获取对象:将本对象引用到的其他对象全部挪到灰色集合中,最后将本对象本身挪到黑色集合里面;
重复步骤 3),直至灰色集合为空时结束。结束后,仍在白色集合的对象即为 GC Roots 不可达,可以进行回收。
15、什么是多标和漏标?
当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
多标
当某个对象标记成灰色时,不被引用对象继续引用,本该被回收却被当作存活对象继续遍历下去,产生浮动垃圾。这并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。
漏标
假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程先将 E 与 G 的引用关系断开,再让 D 引用到 G。切回GC线程继续执行回收算法时,因为 E 已经没有对 G 的引用了,所以不会将 G 放到灰色集合;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。最后导致 G 被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
16、如何解决漏标问题?
从代码角度看,漏标只有同时满足以下逻辑时才会发生:
var G = objE.fieldG;//读
objE.fieldG = null;//写
objD.fieldG = G;//写
1)读取灰色对象的成员变量属性值 ref;
2)将灰色对象对应属性设置为 null;
3)将属性 ref 赋值给黑色对象。
不难看出,只要在上面这三步中的任意一步中做一些手脚,通过读写屏障将引用对象记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),再对该集合的对象遍历即可(重新标记)。
重新标记通常是需要 STW 的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。
在现代垃圾回收器中,不同的收集器对漏标的处理方案有所不同:
G1 采用写屏障 + SATB
当原来成员变量的引用发生变化之前,记录下原来的引用对象,即原始快照(Snapshot At The Beginning,SATB)。
CMS 采用写屏障 + 增量更新
当有新引用插入进来时,记录下新的引用对象,这种做法的思路是不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
ZGC 采用读屏障
读取成员变量时一律记录下来,这种做法是保守的,但也是安全的。
4 类加载
1、什么是类加载机制?
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
2、讲讲类加载的过程?
在 Java 中,一个类从定义到使用可以分为加载、验证、准备、解析、初始化、使用以及卸载几个步骤。其中验证、准备和解析又可以统称为连接。
加载
1)通过全限定类名来获取定义此类的二进制字节流;
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
1)文件格式验证:此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
2)元数据验证:此阶段保证不存在不符合 Java 语言规范的元数据信息。如是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
4)符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
同时为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始。
初始化
到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行
此过程不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的
3、类初始化顺序?
1)父类静态变量/静态初始化块 - 子类静态变量/静态初始化块;
2)父类变量/初始化块 - 父类构造器;
3)子类变量/初始化块 - 子类构造器。
4、类加载时机?
对于加载,Java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则严格规定了以下几种情况必须立即对类进行初始化,如果类没有进行过初始化,则需要先触发其初始化。
1)遇到 new(用 new 实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时;
2)使用 Java.lang.reflect 反射包的方法对类进行反射调用时,如果此时类没有进行 init,会先 init;
3)当初始化一个类时,如果其父类没有进行初始化,先初始化父类;
4)JVM 启动时,用户需要指定一个执行的主类(包含 main 的类),虚拟机会先执行这个类;
5)当使用 JDK 1.7 的动态语言支持的时候,当 java.lang.invoke.MethodHandler 实例后的结果是 REF-getStatic/REF_putstatic/REF_invokeStatic 的句柄,并且这些句柄对应的类没初始化的话应该首先初始。
以上这 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用,如:
1)通过子类引用父类的静态字段,不会导致子类初始化。
2)通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
5、什么是类加载器?
类加载器是负责将可能是网络上、也可能是磁盘上的 class 文件加载到内存中,并为其生成对应的 java.lang.class 对象。一旦一个类被载入 JVM 了,同一个类就不会被再次加载。
在 JAVA 中一个类用其全限定类名(包名和类名)作为其唯一标识,但是在 JVM 中,一个类用其全限定类名和其类加载器作为其唯一标识。即在 JAVA 中的同一个类,如果用不同的类加载器加载,则生成的 class 对象认为是不同的。
6、有哪些类加载器?
当 JVM 启动时,会形成由三个类加载器组成的初始类加载器层次结构:
启动类加载器(BootstrapClassLoader)
是嵌在 JVM 内核中的加载器,该加载器是用 C++ 语言编写,主要负载加载 JAVA_HOME/lib 下的类库,启动类加载器无法被应用程序直接使用。
扩展类加载器(ExtensionClassLoader)
该加载器器是用 JAVA 编写,且它的父类加载器是 Bootstrap。主要加载 JAVA_HOME/lib/ext 目录中的类库。也可通过 -Djava.ext.dirs= 参数设置加载路径。
系统类加载器(AppClassLoader)
系统类加载器,也称为应用程序类加载器,负责加载应用程序 classpath 目录下的所有 jar 和 class 文件。它的父加载器为 ExtClassLoader。
此外还有:
自定义类加载器
实现自定义类加载器分为两步:一是继承 java.lang.ClassLoader;二是重写父类的 findClass() 方法。
线程上下文类加载器
为解决基础类无法调用类加载器加载用户提供代码的问题,Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器默认就是 Application 类加载器,并且可以通过 java.lang.Thread.setContextClassLoaser() 方法进行设置。
7、什么是双亲委派模型?
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型对于保证 Java 程序的稳定运作很重要,例如 java.lang.Object 这个类,它存放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
8、如何破坏双亲委派模型?
双亲委派机制原则在 loadClass() 方法中,只需要绕开该方法中即可。
使用自定义类加载器,重写 loadClass() 方法。(注意不是 findClass() 方法)
给当前线程设定关联类加载器(线程上下文类加载器),使用 SPI(Service Provider Interface ) 机制绕开 loadclass() 方法。
第二种方法已被广泛应用,如 JDBC、Dubbo 等。
5 调优
1、有哪些调优工具?
JDK 自带工具
首先,JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
1)jps:与 Linux 上的 ps 类似,用于查看有权访问的虚拟机的进程,并显示他们的进程号。当未指定 hostid 时,默认查看本机 JVM 进程。
2)jinfo:可以输出并修改运行时的 Java 进程的一些参数。
3)jstat:可以用来监视 JVM 内存内的各种堆和非堆的大小及其内存使用量。
4)jstack:堆栈跟踪工具,一般用于查看某个进程包含线程的情况。
5)jmap:打印出某个 JVM 进程内存内的所有对象的情况,一般用于查看内存占用情况。
6)jconsole:一个 GUI 监视工具,可以以图表化的形式显示各种数据,并支持远程连接。
Eclipse Memory Analyzer(MAT)
是一款内存分析工具,利用 dump 分享内存泄漏。
2、有哪些常用的调优参数?
常用的设置
1)-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
2)-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
3)-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。
4)-XX:NewSize=n:设置年轻代初始化大小大小。
5)-XX:MaxNewSize=n:设置年轻代最大值。
6)-XX:NewRatio=n:设置年轻代和年老代的比值。如 n = 3 时表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4。
7)-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8。
8)-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。
9)-XX:ThreadStackSize=n:线程堆栈大小。
10)-XX:PermSize=n:设置持久代初始值。
11)-XX:MaxPermSize=n:设置持久代大小。
12)-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。
不常用的设置
1)-XX:LargePageSizeInBytes=n:设置堆内存的内存页大小。
2)-XX:+UseFastAccessorMethods:优化原始类型的 getter 方法性能。
3)-XX:+DisableExplicitGC:禁止在运行期显式地调用S ystem.gc(),默认启用。
4)-XX:+AggressiveOpts:是否启用 JVM 开发团队最新的调优成果。如编译优化,偏向锁,并行年老代收集等,JDK 6 之后默认启动。
5)-XX:+UseBiasedLocking:是否启用偏向锁,JDK 6 默认启用。
6)-Xnoclassgc:是否禁用垃圾回收。
7)-XX:+UseThreadPriorities:使用本地线程的优先级,默认启用。
回收器相关设置
1)-XX:+UseSerialGC:设置串行收集器,年轻带收集器。
2)-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK 5 以上 JVM 会根据系统配置自行设置,所以无需再设置此值。
3)-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量。
4)-XX:+UseParallelOldGC:设置并行年老代收集器,JDK 6 支持对年老代并行收集。
5)-XX:+UseConcMarkSweepGC:设置年老代并发收集器。
6)-XX:+UseG1GC:设置 G1 收集器,JDK 9 默认垃圾收集器
1. 内存模型以及分区,需要详细到每个区放什么。
JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面, class 类信息常量池(static 常量和 static 变量)等放在方法区
new:
方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字 节码)等数据
堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要 在堆上分配
栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操 作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所 以还是一个指向地址的指针
本地方法栈:主要为 Native 方法服务
程序计数器:记录当前线程执行的行号
2. 堆里面的分区:Eden,survival (from+ to),老年代,各自的特点。
堆里面分为新生代和老生代(java8 取消了永久代,采用了 Metaspace),新生代包 含 Eden+Survivor 区,survivor 区里面分为 from 和 to 区,内存回收时,如果用的是复 制算法,从 from 复制到 to,当经过一次或者多次 GC 之后,存活下来的对象会被移动 到老年区,当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区 当新生区满了之后会触发 YGC,先把存活的对象放到其中一个 Survice 区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎 片,因此一般会把 Eden 进行完全的清理,然后整理内存。那么下次 GC 的时候, 就会使用下一个 Survive,这样循环使用。如果有特别大的对象,新生代放不下, 就会使用老年代的担保,直接放到老年代里面。因为 JVM 认为,一般大对象的存 活时间一般比较久远。
3. 对象创建方法,对象的内存分配,对象的访问定位。
new 一个对象
4. GC 的两种判定方法:
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就 会回收但是 JVM 没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A) 的情况
引用链法: 通过一种 GC ROOT 的对象(方法区中静态变量引用的对象等-static 变 量)来判断,如果有一条链能够到达 GC ROOT 就说明,不能到达 GC ROOT 就说明 可以回收
5. SafePoint 是什么
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始 执行 GC
1. 循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入 safepoint)
2. 方法返回前
3. 调用方法的 call 之后
4. 抛出异常的位置
6. GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用 在什么地方,如果让你优化收集方法,有什么思路?
先标记,标记完毕之后再清除,效率不高,会产生碎片
复制算法:分为 8:1 的 Eden 区和 survivor 区,就是上面谈到的 YGC
标记整理:标记完毕之后,让所有存活的对象向一端移动
7. GC 收集器有哪些?CMS 收集器与 G1 收集器的特点。
并行收集器:串行收集器使用一个单独的线程进行收集,GC 时服务有停顿时间
串行收集器:次要回收中使用多线程来执行
CMS 收集器是基于“标记—清除”算法实现的,经过多次标记才会被清除
G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间) 上来看是基于“复制”算法实现的
8. Minor GC 与 Full GC 分别在什么时候发生?
新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC
9. 几种常用的内存调试工具:jmap、jstack、jconsole、jhat
jstack 可以看当前栈的情况,jmap 查看内存,jhat 进行 dump 堆的信息 mat(eclipse 的也要了解一下)
10. 类加载的几个过程:
加载、验证、准备、解析、初始化。然后是使用和卸载了
通过全限定名来加载生成 class 对象到内存中,然后进行验证这个 class 文件,包括文 件格式校验、元数据验证,字节码校验等。准备是对这个对象分配内存。解析是将符 号引用转化为直接引用(指针引用),初始化就是开始执行构造器的代码
11.JVM 内存分哪几个区,每个区的作用是什么?
java 虚拟机主要分为以下一个区:
方法区:
1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里 进行的 GC 主要是对方法区里的常量池和对类型的卸载
2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后 的代码等数据。
3. 该区域是被线程共享的。
4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池 具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量 池中。
虚拟机栈:
1. 虚拟机栈也就是我们平常所称的栈内存,它为 java 方法服务,每个方法在执行的时候都 会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
2. 虚拟机栈是线程私有的,它的生命周期与线程相同。
3. 局部变量表里存储的是基本数据类型、returnAddress 类型(指向一条字节码指令的地 址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表 对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
4.操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索 引来访问,而是压栈和出栈的方式
5.每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了 支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接 引用。
本地方法栈
本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。
堆
java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这 里创建,因此该区域经常发生垃圾回收操作。
程序计数器
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码 指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内 存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。
12.如和判断一个对象是否存活?(或者 GC 对象的判定方 法)
判断一个对象是否存活有两种方法:
1. 引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象 时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说 明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回 收,所以主流的虚拟机都没有采用这种算法。
2.可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
在 java 中可以作为 GC Roots 的对象有以下几种:
虚拟机栈中引用的对象
方法区类静态属性引用的对象
方法区常量池引用的对象
本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一 定会被回收。当一个对象不可达 GC Root 时,这个对象并 不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记
如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行 一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法 或者已被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队 列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承 诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行 第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
13.简述 java 垃圾回收机制?
在 java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚 拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将 它们添加到要回收的集合中,进行回收。
14.java 中垃圾收集的方法有哪些?
1. 标记-清除:
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被 回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不 高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在 分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
2. 复制算法:
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只 使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然 后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式, 内存的代价太高,每次基本上都要浪费一般的内存。
于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。 每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然 后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对 象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)
3. 标记-整理
该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高 时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回 收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
4. 分代收集
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生 代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那 么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担 保,所以可以使用标记-整理 或者 标记-清除。
15.java 内存模型
java 内存模型(JMM)是线程间通信的控制机制.JMM 定义了主内存和线程之间抽象关系。 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地 内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬 件和编译器优化。
线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:
1. 首先,线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
2. 然后,线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。
16.java 类加载过程?
java 类加载需要经历一下 7 个过程:
加载
加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:
1. 通过一个类的全限定名获取该类的二进制流。
2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
3. 在内存中生成该类的 Class 对象,作为该类的数据访问入口。
验证
验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成 以下四钟验证:
1. 文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟 机范围内,常量池中的常量是否有不被支持的类型。
2. 元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不 被继承的类等。
3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析, 确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转 指令是否正确等。
4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执 行。
准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进 行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象 一起分配在 Java 堆中。
public static int value=123;//在准备阶段 value 初始值为 0 。在初始化阶段才会变 为 123 。
解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之 前,也有可能在初始化之后。
初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过 自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正 开始执行类中定义的 Java 程序代码。
17. 简述 java 类加载机制?
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最 终形成可以被虚拟机直接使用的 java 类型。
18. 类加载器双亲委派模型机制?
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类 去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
19.什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器
1. 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,无法被 java 程序直接 引用。
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的 实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH) 来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
4. 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。
20.简述 java 内存分配与回收策率以及 Minor GC 和 Major GC
1. 对象优先在堆的 Eden 区分配。
2. 大对象直接进入老年代.
3. 长期存活的对象将直接进入老年代.
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC.Minor Gc 通 常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高, 回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。