1、运行时数据区是什么?
jvm运行java程序时会把它管理的内存划分为若干个不同的数据区,这些区域有各自的用途,创建和销毁时间。
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享:堆,方法区
2、程序计数器是什么?
是一块较小的内存空间,可以看作当前线程执行字节码的行号指令器。字节码解释器通过改变计数器的值选取下一条执行指令。是唯一在虚拟机规范中没有规定内存溢出情况的区域。
线程在执行java方法时,程序计数器执行的是字节码指令地址;执行本地方法时,计数器值为Undefined。
3、虚拟机栈的作用
描述java方法的内存模型,当有新线程创建时,分配一个栈空间,线程结束后空间回收,栈与线程具有相同的生命周期。栈中元素用于支持方法调用,每当有方法调用时都会创建一个栈帧,用于储存方法中的局部变量,操作栈,动态链接等信息,每个方法的调用和执行完成,就是栈帧入栈和出栈的过程。
两种异常:线程请求的栈深度大于虚拟机允许的深度,抛出StackOverFlowError;
如果栈容量可以扩展,栈扩展无法申请到足够内存,抛出OutOfMemoryError。
4、本地方法栈的作用
与虚拟机栈相似,只是为本地方法服务。Hotspot将本地方法栈和虚拟机栈合二为一。
5、堆的作用
是jvm管理的最大一块内存区域,在虚拟机启动时创建,线程共享。存储的是对象实例。堆可以处于物理上不连续的内存空间,逻辑上应该连续。
6、方法区作用
用于存储被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存。
在jdk8之前,采用永久代实现方法区,容易内存溢出,因为永久代有上限。jdk7将字符串常量池和静态变量移除,jdk8利用本地内存的元空间实现方法区。
垃圾回收很少在方法区出现,主要目标针对类型和常量池卸载。
7、运行时常量池作用
是方法区中的一部分,Class文件有一项信息叫常量池表,存储编译器生成的字面量和符号引用,这部分内容在类加载后存放到运行时常量池,同时也会将符号引用解析的直接引用存放到此。
常量不仅可以编译期产生,运行时产生的新常量也放到池中,比如String的intern方法
8、直接内存
直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但是这部分内存使用频繁并且可能导致内存溢出。
在jdk1.4中引入了NIO这种基于通道和缓冲区的IO,他可以使用Native函数库直接分配堆外内存,通过堆中DirectByteBuffer作为内存的引用,避免在java堆和Native堆来回复制数据。
直接内存分配不受堆大小限制,会收到本机总内存限制。由直接内存导致的OOM,明显的特征是Heap Dump中不会看到明显的异常。
9、内存溢出和内存泄漏区别
内存溢出:程序申请内存时,没有足够内存供其使用
内存泄露:程序申请内存后,无法释放已申请的内存,最终导致内存溢出。
10、创建对象的过程
● 首先java虚拟机遇到new指令,先检查指令的参数是否能在常量池中定位到一个类的符号引用;
● 然后检查这个符号引用对应的类是否被加载、解析和初始化过,如果没有先进行类加载;
● 接下来进行内存分配,针对堆内存是否规整,有两种方式
ⅰ. 指针碰撞,对于绝对规整的堆内存,使用过的内存放在一边,空闲的内存放在一边,中间放一个指针作为分界。分配内存仅仅是将指针向空闲内存挪动一段与对象大小相等的距离。
ⅱ. 空闲列表,对于不规整的堆内存,虚拟机必须维护一个列表,记录哪些内存块可用。分配内存的时候在列表中找到一块足够大的空间分配给对象,然后更新列表上的记录。
ⅲ. 而对于指针碰撞需要考虑线程安全问题。在并发情况下可能存在正在给对象A分配内存,指针还没来得及修改,对象B又使用原来的指针进行内存分配。解决方案有两种:a.对分配内存的动作进行同步处理,实际上虚拟机采用CAS配上失败重试保证更新操作的原子性。b.把分配内存的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。
● 虚拟机将分配的内存空间(不包括对象头)初始化为0,保证对象的实例字段不赋初值就可以使用;
● 初始化对象头,包括类的元数据信息,哈希码,GC分代年龄等;
● 执行<init>()方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把实例对象的首地址赋值给应用变量。
11、对象的内存布局
对象头,实例数据,对齐填充
● 对象头(存放两类信息)
a. 对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
b. 类型指针,对象指向他的类型元数据的指针,虚拟机通过指针确定该对象是那个类的实例。如果对象是数组,对象头还要记录数组的长度。
● 实例数据
对象的有效信息,包括本类中成员变量以及所有可见的父类中成员变量。存储顺序受到虚拟机分配策略和字段在源码中定义顺序的影响。相同宽度的字段定义在一起,在满足该前提条件下,父类变量出现在子类之前。
● 对齐填充
用于对齐填充,因为java虚拟机规定对象的起始地址必须是8字节的整数倍。
12、对象的访问方式
句柄:堆会划分一块内存作为句柄池,引用中存储的是对象的句柄地址,句柄包含对象的实例数据和类型数据的地址信息。优点是引用中存放的是稳定的句柄地址,在GC过程中对象被移动,只会改变句柄实例数据的指针,而引用不用改变。
直接引用:引用中存储的直接就是对象地址,堆中对象的内存布局必须考虑如何放置类型数据的信息。优点是速度更快,如果只访问对象本身的话,减少一次指针定位开销。
13、如何判断对象是否是垃圾
引用计数:在对象中添加一个计数器,如果被引用计数器加一,引用失效计数器减一,当计数器为0则被标记为垃圾。原理简单,效率高,但在java中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。
可达性分析:基本思路是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜素,搜素过程所走过的路程称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
14、GC Roots有哪些
● 虚拟机栈中引用的对象
● 方法区中类静态属性引用的对象
● 方法区中常量引用的对象
● 本地方法栈中JNI(Native方法)引用的对象
● java虚拟机内部引用
● 所有被同步锁持有的对象
● 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
15、标记为垃圾的对象什么时候回收
即使在可达性分析算法中判定为不可达对象,JVM也不是立马对其进行回收。因为要真正宣告一个对象死亡,至少要经历两次标记过程:第一次是在可达性分析后发现没有与GC Roots相连接的引用链,会被标记一次。
随后针对是否有必要执行finalize()方法进行筛选,如果未覆盖finalize()或者该方法已经被调用过,视为没有必要执行。
如果有必要执行,将对象放在F-Queue队列中,稍后进行第二次小规模标记,如果对象在finalize()方法拯救了自己,将会被移除即将回收的集合。
16、四个引用
强引用
最传统的引用的定义,类似“Object object = new Object()”。只要引用关系存在,就不会被回收
软引用
弱于强引用,系统即将发生内存溢出异常前,把这些对象列进回收范围之中进行第二次回收。JDK1.2版本之后提供了SoftReference类实现软引用。
弱引用
弱于软引用,垃圾收集器开始工作,无论内存是否足够,都会回收弱引用关联的对象。WeakReference。
虚引用
虚引用的存在不会对其生存时间构成影响,无法通过虚引用获得一个对象实例。目的是能在这个对象被收集器回收时收到一个系统通知。PhantomReference。
17、有哪些垃圾回收算法
标记清除(老年代)
算法分为“标记”和“清除”两个阶段:首先标记所有需要回收的对象,在标记完成之后,统一回收掉所有被标记的对象,也可以反过来。
缺点:1、执行效率不稳定,如果java堆中包含大量对象,且大部分要被回收,这时必须进行大量标记和清除,执行效率会随着对象数量增加而降低。
2、内存空间的碎片化问题,标记清除之后产生大量不连续的内存碎片,空间碎片太多可能会导致以后分配较大对象时无法找到足够连续的内存而不得不提前触发下一次垃圾收集。
标记复制(新生代)
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后把已使用的内存空间一次清理掉。
优点:不用考虑空间碎片化问题,方法简单,运行高效。
缺点:内存浪费严重
现在大多java虚拟机采用这种算法回收新生代。由于新生代中98%对象熬不过第一轮收集,所以“Apple式回收”将新生代分为Eden空间和两个Survivor空间(8:1:1)。每次分配内存只用Eden和一个Survivor,当发生Minor GC时将Eden和一个Survivor中存活的对象拷贝到另一块Survivor中,最后清理掉Eden和一个Survivor空间。当Survivor不够用时,对象会通过分配担保机制直接进入老年代。
为什么需要两个survivor?因为如果只有一个,第一次GC后将存活的对象放到survivor中,第二次垃圾回收这个survivor中存活对象没地方复制。
标记整理(老年代)
标记整理算法与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清除点边界以外的内存。本质在于前者是一种非移动式回收算法,后者是移动式。
18、分代收集理论
目前常用垃圾收集器一般采用分代收集理论进行设计,将java堆内存分为新生代和老年代,根据各个年代的特点采用合适的算法。分代收集理论建立在两个假说之上
弱分代假说:绝大多数对象都是朝生夕灭的
强分代假说:熬过越多次垃圾收集的对象越难以消亡。
Minor GC:目标只是新生代的垃圾收集
Major GC:目标只是老年代的垃圾收集,目前只有CMS有单独收集老年代行为。
Full GC:目标是整个堆和方法区的垃圾收集。
发生FullGC情况:老年代无法再分配内存;元空间不足;显示调用System.gc。
19、经典垃圾收集器
- Serial收集器
Serial是最基础的新生代收集器,使用标记复制算法。工作时,会停掉所有工作线程,用一个单线程去完成GC工作。
特点:简单高效,额外内存消耗最小,适合jvm管理内存不大的情况,是虚拟机在客户端模式下默认的新生代收集器。
- ParNew收集器
实质是Serial收集器的多线程并行版本,除了使用多条线程进行垃圾回收,其余策略与Serial完全一样。除了Serial外,目前只有它能与CMS收集器配合工作。
- Parallel Scavenge收集器
新生代收集器,使用标记复制算法,可并行的多线程收集器,与ParNew类似。
关注点与其他收集器不同,目标是达到一个可控时间的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
- Serial Old收集器
是serial收集器的老年代版本,单线程收集器,使用标记-整理算法。主要意义是供客户端模式下的HotPot虚拟机使用。
服务端模式下,有两种用途:1、在JDK5及之前版本中,与Parallel Scavenge搭配使用;2、作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure(并发失败)时使用。
- Parallel Old收集器
是Parallel Scavange收集器的老年代版本,支持多线程并行收集,使用标记整理算法。
在注重吞吐量或者处理器资源较为稀缺的场合,可以优先考虑Parallel Scavange加Parallel Old组合
- CMS收集器
CMS(Concurrent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器,使用标记清除算法。
过程分为四步:
● 初始标记:只标记GC Roots能直接关联的对象,速度很快。
● 并发标记:从GC Roots直接关联对象遍历整个对象图,耗时较长但是不需要停顿用户线程。
● 重新标记:为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录。
● 并发清除:清除标记阶段判断已经死亡的对象
缺点:
● 对处理器资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是因为占用一些线程导致应用程序变慢,降低总吞吐量。CMS默认启动回收线程数是(处理器核心数量+3)/4,当处理器核心数不足4时,CMS对用户程序影响很大。
● CMS无法处理浮动垃圾,可能出现Concurrent Mode Failure(并发失败)继而导致Full GC的发生。浮动垃圾是在并发标记和并发清理阶段产生的垃圾。由于垃圾收集阶段用户线程还在继续,就需要预留足够内存提供给用户线程,因此CMS不能像其他收集器那样等老年代几乎完全填满再进行收集,必须预留一部分空间供并发收集时的程序使用。如果预留内存不足,会导致并发失败。
● 标记清除算法会导致大量空间碎片的产生
提供参数-XX:CMSFullGCsBeforeCompaction,要求CMS收集器在执行若干次不整理空间的FullGC后,下一次进入FullGC前先进行碎片整理。
- Garbage First收集器
开创了收集器面向局部收集的设计思路和基于region的内存布局,主要面向服务端,最初设计目标是替换CMS。
G1可面向堆任何部分进回收,衡量标准不再是分代,而是哪块内存中的垃圾数量最多,回收收益最大。
跟踪各 Region里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护⼀个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。
G1 运作过程:
初始标记:标记 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时能正确地在可用Region中分配新对象。需要STW但耗时很短,在Minor GC时同步完成。
并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理SATB记录的在并发时有变动的对象。
最终标记:对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量SATB记录。
筛选回收:对各Region的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。
20、ZGC了解吗
JDK11 中加⼊的具有实验性质的低延迟垃圾收集器,⽬标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。
基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记整理,以低延迟为首目标。
ZGC 的Region具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的
21、内存分配与回收策略
● 对象优先在Eden分配
● 大对象直接进入老年代
通过-XX:PretenureSizeThreshold参数指定大于该设置值的对象直接在老年代分配。
● 长期存活的对象将进入老年代
对象晋升老年代的年龄阈值,通过参数-XX:MaxTenuringThreshold设置。
● 动态对象年龄判断
如果在Surivivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代。
● 空间分配担保
在发生Minor GC之前,虚拟机检查
JDK6 Update24之后取消分配担保的判断。
22、故障处理工具
- jps(JVM Process Status Tool):虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID。
- jstat(JVM Statistic Monitoringn Tool):虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息的工具。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据
- jinfo(Configuration Info for Java):java配置信息工具
实时查看和调整虚拟机各项参数
- jmap(Memory Map for java):Java内存映像工具
用于生成堆转储快照(heapdump),还可以查询finalize执行队列、Java堆和方法区详细信息,如空间使用率,当前使用哪种收集器
- jhat(JVM Heap Analysis Tool):虚拟机堆转存快照分析工具
与jmap搭配使用,分析jmap生成的堆转存快照。
- jstack(Stack Trace for Java):Java堆栈跟踪工具
用于生成虚拟机当前时刻的线程快照(threaddump)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
23、类加载过程
a.加载
需要完成三件事:
1)通过一个类的全限定名获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
b.验证
目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证运行后不会危害虚拟机安全。
该阶段完成以下四个阶段检验动作:
1)文件格式验证
验证字节流是否符合Class文件格式规范,并且是否能被当前版本的java虚拟机处理
2)元数据验证
对类的元数据信息进行语义校验,保证符合《java语言规范》要求。包括:此类是否有父类;是否继承了不允许被继承的类等。
3)字节码验证
通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。
4)符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候,目的是确保解析行为能够正常执行。
对于反复使用和验证过的代码,可以通过-Xverify:none参数关闭类验证,减少类加载时间。
c.准备
正式为类中定义的变量(静态变量)分配内存并设置类变量初始值。
d.解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程。不一定在初始化之前,也可能在初始化之后进行。
e.初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,可以赋值,但是不能访问。
2)Java虚拟机保证在子类<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。
3)<clinit>()方法对于类和接口不是必需的,如果类中没有静态语句块,也没有对变量的赋值动作,那么编译器可以不为这个类生成<clinit>()方法;
4)执行接口的<clinit>()方法,不需要先执父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用,父接口才会被初始化。此外接口实现类在初始化时也不会执行接口的<clinit>()方法;
5)同一个类加载器下,一个类型只会被初始化一次。
类进行初始化的情况,有且只有以下六种:
1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类型没有进行初始化过,则需要先触发其初始化。
能够生成这四条指令的java场景:
a.使用new关键字创建实例对象;
b.读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放到常量池的静态字段除外)
c.调用一个类型的静态方法
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现父类没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
5)当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类未初始化过,则需要先触发其初始化。
6)当一个接口中新定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
f.使用
g.卸载
24、有哪些类加载器
类加载器实现加载阶段需要完成的动作:通过类的全限定名称获取描述该类的二进制字节流。
对于任意一个类,必须由加载他的类加载器和这个类本身共同确定其在java虚拟机中的唯一性
1、启动类加载器(Bootstrap Class Loader)
负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合类库的即使放到lib目录中也不会被加载)类库加载到虚拟机内存中。
2、扩展类加载器(Extension Class Loader)
这个类加载器是由类sun.misc.Launcher$ExtClassLoader中以java代码形式实现的,开发者可以直接在程序中使用扩展类加载器。
负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
3、应用程序类加载器(Application Class Loader)
这个类加载器是由类sun.misc.Launcher$AppClassLoader实现的。由于它时ClassLoader类中的getSystemClassLoader()方法的返回值,所以也称他为“系统类加载器”。
负责加载用户类路径(ClassPath)上所有的类库。
25、双亲委派机制
双亲委派模型要求除了顶层的启动类加载器外,其余的加载器都应有自己的父加载器。但是这里的父子关系不是用继承关系实现的,通常使用组合关系来复用父加载器代码。
工作过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
26、为什么采用双亲委派机制
java虚拟机只会在不同的类的类名相同且加载该类的加载器均相同的情况下才会判定这是一个类。如果没有双亲委派机制,同一个类可能就会被多个类加载器加载,如此类就可能会被识别为两个不同的类,相互赋值时问题就会出现。
双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同
27、打破双亲委派机制的案例
1.Tomcat可以加载自己目录下的class文件,并不会传递给父类加载器
2.java的SPI,是一种父类加载器请求子类加载器完成加载的行为。
如何破坏:
双亲委派机制原则在loadclass方法中。
只需要绕开loadclass方法中即可。
1.自定义类加载器 ,重写loadclass方法
2.SPI机制绕开loadclass 方法。当前线程设定关联类加载器。