Java 虚拟机面试题(爪哇程序员)
栈帧里面包含哪些东西?
局部变量表、操作数栈、动态连接、返回地址等
程序计数器有什么作用?
程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示 器。这里面存的,就是当前线程执行的进度。
程序计数器还存储了当前正在运行的流程,包括正在执行的指令、跳转、分支、循环、异常处 理等。
字符串常量存放在哪个区域?
-
字符串常量池,已经移动到堆上(jdk8之前是perm区),也就是执行intern方法后存的 地方。
-
类文件常量池,constant_pool,是每个类每个接口所拥有的,这部分数据在方法区, 也就是元数据区。而运行时常量池是在类加载后的一个内存区域,它们都在元空间。
你熟悉哪些垃圾收集算法?
标记清除(缺点是碎片化)
复制算法(缺点是浪费空间)
标记整理算法(效率比前两者差)
分代收集算法(老年代一般使用“标记-清除”、“标记-整理”算法,年轻代一般用复制算法)
Java里有哪些引用类型?
强引用
这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉。
软引用
软引用用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内 存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会 抛出内存溢出异常。 可以看到,这种特性非常适合用在缓存技术上。比如网页缓存、图片缓存等。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾 回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用
弱引用对象相比较软引用,要更加无用一些,它拥有更短的生命周期。当JVM进行垃圾回收 时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期,在 Java 中,用 java.lang.ref.WeakReference 类来表示。它的应用场景和软引用类似,可以在一些对 内存更加敏感的系统里采用。
虚引用
这是一种形同虚设的引用,在现实场景中用的不是很多。虚引用必须和引用队列 (ReferenceQueue)联合使用。如果一个对象仅持有虚引用,那么它就和没有任何引用一 样,在任何时候都可能被垃圾回收。实际上,虚引用的 get,总是返回 null。
JVM怎么判断一个对象是不是要回收?
引用计数法(缺点是对于相互引用的对象,无法进行清除)
可达性分析
GC Roots有哪些?
GC Roots 是一组必须活跃的引用。用通俗的话来说,就是程序接下来通过直接引用或者间接 引用,能够访问到的潜在被使用的对象。
GC Roots 包括:
Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我 们栈帧相关的各种引用。
所有当前被加载的 Java 类。
Java 类的引用类型静态变量。
运行时常量池里的引用类型常量(String 或 Class 类型)。
JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类。
用于同步的监控对象,比如调用了对象的 wait() 方法。
JNI handles,包括 global handles 和 local handles。
这些 GC Roots 大体可以分为三大类,下面这种说法更加好记一些:
活动线程相关的各种引用。
类的静态变量的引用。
JNI 引用。
有两个注意点:
我们这里说的是活跃的引用,而不是对象,对象是不能作为 GC Roots 的。
GC 过程是找出所有活对象,并把其余空间认定为“无用”;而不是找出所有死掉的对象,并回 收它们占用的空间。所以,哪怕 JVM 的堆非常的大,基于 tracing 的 GC 方式,回收速度也会 非常快。
对象都是优先分配在年轻代上的吗?
不是。当新生代内存不够时,老年代分配担保。而大对象则是直接在老年代分配。
你了解过哪些垃圾收集器?
年轻代
Serial 垃圾收集器(单线程,通常用在客户端应用上。因为客户端应用不会频繁创建很多对 象,用户也不会感觉出明显的卡顿。相反,它使用的资源更少,也更轻量级。)
ParNew 垃圾收集器(多线程,追求降低用户停顿时间,适合交互式应用。)
Parallel Scavenge 垃圾收集器(追求 CPU 吞吐量,能够在较短时间内完成指定任务,适合没 有交互的后台计算。)
老年代
Serial Old 垃圾收集器
Parallel Old垃圾收集器
CMS 垃圾收集器(以获取最短 GC 停顿时间为目标的收集器,它在垃圾收集时使得用户线程和 GC
说说CMS垃圾收集器的工作原理
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短 垃圾回收停顿时间, 和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算 法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
1)初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 2)并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
3)重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记 录,仍然需要暂停所有的工作线程。
4)并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
说说G1垃圾收集器的工作原理
优点:指定最大停顿时间、分Region的内存布局、按收益动态确定回收集
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收 集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及 固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region), 每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。 收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已 经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列 区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将 Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样 可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去 跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需 时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使 用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的 那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及 具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
G1收集器的运作过程大致可划分为以下四个步骤: ·
初始标记 (Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。 这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1 收集器在这个阶段实际并没有额外的停顿。
并发标记 (Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描 整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象 图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记 (Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍 遗留下来的最后那少量的SATB记录。
筛选回收 (Live Data Counting and Evacuation):负责更新Region的统计数据,对各个 Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选 择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的 Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂 停用户线程,由多条收集器线程并行完成的。
从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的 。
说说ZGC垃圾收集器的工作原理
1)内存布局
小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于 4MB的对象。
大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于 放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫 作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。大型 Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收 集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。
2)染色指针
染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储 额外信息呢?在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节 [3] 。实际上,基于需求(用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多) 和成本(消耗更多晶体管)的考虑,在AMD64架构 [4] 中只支持到52位(4PB)的地址总线 和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存只有 256TB。此外,操作系统一侧也还会施加自己的约束,64位的Linux则分别支持47位 (128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统甚 至只支持44位(16TB)的物理地址空间。
尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今 天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46 位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指 针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过 finalize()方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有46位的地址空 间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂) 。
3)收集过程
并发标记 (Concurrent Mark):与G1、Shenandoah一样,并发标记是遍历对象图做可达 性分析的阶段,前后也要经过类似于G1、Shenandoah的初始标记、最终标记(尽管ZGC中的 名字不叫这些)的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。与G1、 Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色 指针中的Marked 0、Marked 1标志位。
并发预备重分配 (Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件 统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。重分配集与G1收集器的回收集(Collection Set)还是有区别的,ZGC划分Region的 目的并非为了像G1那样做收益优先的增量回收。相反,ZGC每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在 JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。
并发重分配 (Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把 重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表 (Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集 器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位 于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发 表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象, ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。这样做的好处是只有第一次访问 旧对象会陷入转发,也就是只慢一次,对比Shenandoah的Brooks转发指针,那是每次对象访 问都必须付出的固定开销,简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比 Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集 中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但 是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系, 这些旧指针一旦被使用,它们都是可以自愈的。
并发重映射 (Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对 象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的 并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可 以自愈的,最多只是第一次 使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结 束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并 发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它 们都是要遍历所有对象的,这样合并就节省了一次遍历对象图 [9] 的开销。一旦所有指针都被 修正之后,原来记录新旧对象关系的转发表就可以释放掉了。
ZGC收集器中的染色指针有什么用?
染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储 额外信息呢?在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节 [3] 。实际 上,基于需求(用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多) 和成本(消耗更多晶体管)的考虑,在AMD64架构 [4] 中只支持到52位(4PB)的地址总线 和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存只有 256TB。此外,操作系统一侧也还会施加自己的约束,64位的Linux则分别支持47位 (128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统甚 至只支持44位(16TB)的物理地址空间。
尽管Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今 天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46 位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指 针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过 finalize()方法才能被访问到。当然,由于这些标志位进一步压缩了原本就只有46位的地址空 间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂) 。
说说类加载的过程
加载
验证
准备(为一些类变量分配内存,并将其初始化为默认值)
解析(将符号引用替换为直接引用。类和接口、类方法、接口方法、字段等解析)
初始化
说下有哪些类加载器?
Bootstrap ClassLoader(启动类加载器)
Extention ClassLoader(扩展类加载器)
App ClassLoader(应用类加载器)
什么是双亲委派机制?
双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
Tomcat是怎么打破双亲委派机制的呢?
是通过重写ClassLoader#loadClass和ClassLoader#findClass 实现的。可以看图中的 WebAppClassLoader,它加载自己目录下的.class文件,并不会传递给父类的加载器。但 是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。
Java对象的布局了解过吗?
对象头区域此处存储的信息包括两部分:
1、对象自身的运行时数据( MarkWord ),占8字节 存储 hashCode、GC 分代年龄、锁类型标记、偏向锁线程 ID 、 CAS 锁指向线程 LockRecord 的指针等, synconized 锁的机制与这个部分( markwork )密切相关,用 markword 中最低的三位代表锁的状态,其中一位是偏向锁位,另外两位是普通锁位。
2、对象类型指针( Class Pointer ),占4字节 对象指向它的类元数据的指针、 JVM 就是通过它来确定是哪个 Class 的实例。
实例数据区域
此处存储的是对象真正有效的信息,比如对象中所有字段的内容
对齐填充区域
JVM 的实现 HostSpot 规定对象的起始地址必须是 8 字节的整数倍,换句话来说,现在 64 位 的 OS 往外读取数据的时候一次性读取 64bit 整数倍的数据,也就是 8 个字节,所以 HotSpot 为了高效读取对象,就做了"对齐",如果一个对象实际占的内存大小不是 8byte 的整数倍时, 就"补位"到 8byte 的整数倍。所以对齐填充区域的大小不是固定的。
什么情况下会发生栈内存溢出?
栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来 存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型, 对象引用类型。
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方 法递归调用产生这种结果。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机 将抛出一个OutOfMemory 异常。(线程启动过多)。
JVM新生代中为什么要分为Eden和Survivor?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很 快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比 Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的 预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次 Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等 Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分 的存活对象占用连续的内存空间,避免了碎片化的发生)
JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代?
当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来 的对象,则会转移到 Survivor区。
大对象(需要大量连续内存空间的Java对象,如那种很长的字符串)直接进入老年态; 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄 设为1,每熬过一次Minor GC,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。即 长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个 内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢 10倍以上。
什么是指令重排序?
在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会 采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的 情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等 待。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是指令重排。
什么是内存屏障?
内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问 题。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读 取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执 行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷 出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作 执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
什么是happen-before原则?
单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操 作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。 volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任 意操作(当然也包括写操作了)。
happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方 法。 线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程 的检测到中断发送的代码。
线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。
对象创建的happen-before原则: 一个对象的初始化完成先于他的finalize方法调用。
说说你知道的几种主要的JVM参数
1)堆栈配置相关
-Xmx3550m: 最大堆大小为3550m。
-Xms3550m: 设置初始堆大小为3550m。
-Xmn2g: 设置年轻代大小为2g。
-Xss128k: 每个线程的堆栈大小为128k。
-XX:MaxPermSize: 设置持久代大小为16m
-XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久 代)。
-XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个 Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不 经过Survivor区,直接进入年老代。
2)垃圾收集器相关
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20: 配置并行收集器的线程数
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所 以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存 空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是 可以消除碎片
3)辅助信息相关 -XX:+PrintGC 输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K>10414K(130112K), 0.0650971 secs] -XX:+PrintGCDetails 输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
怎么打出线程栈信息?
输入jps,获得进程号。
top -Hp pid 获取本进程中所有线程的CPU耗时性能
jstack pid命令查看当前java进程的堆栈状态
jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件,可以使用fastthread 堆栈定位(fastthread.io)
为什么需要双亲委派模式?
在这里,先想一下,如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object 的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类 的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码。
怎么打破双亲委派模型?
打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。
说一下堆和栈的区别
1)物理地址
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配, 所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年 代使用标记——压缩) 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
2)内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
3)存放的内容
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
4)程序的可见度
堆对于整个应用程序都是共享、可见的。 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
Java 8 为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内 存,受本机可用内存的限制,并且永远不会出现java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只 受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标 志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
说一下Java对象的创建过程
1)类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池 中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化 过。如果没有,那必须先执行相应的类加载过程。
2)分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存 大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是 否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
选择以上2种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决 于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的 是,复制算法内存也是规整的。
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是 很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式 来保证线程安全: CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有 冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败 重试的方式保证更新操作的原子性。 TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首 先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS 进行内存分配
3)初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括 对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程 序能访问到这些字段的数据类型所对应的零值。
4)设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那 个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这 些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象 头会有不同的设置方式。
5)执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生 了,但从 Java 程序的视角来看,对象创建才刚开始,init 方法还没有执行,所有的字段都还 为零。所以一般来说,执行 new 指令之后会接着执行 init 方法,把对象按照程序员的意愿进 行初始化,这样一个真正可用的对象才算完全产生出来。
对象的访问定位有哪几种方式?
建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对 象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针2种:
句柄: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存 储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型 数据的相关信息,而reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句 柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使 用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
说一下堆内存中对象的分配的基本策略
eden区、s0区、s1区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象 的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度 (默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 XX:MaxTenuringThreshold 来设置。
另外,大对象和长期存活的对象会直接进入老年代。
Minor Gc和Full GC 有什么不同呢?
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机 将发起一次Minor GC。
新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一 般也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一 次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
Java会存在内存泄漏吗?请简单描述。
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
如何判断一个类是无用的类?
方法区主要回收的是无用的类,判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类” :
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
介绍一下类文件结构吧!
魔数: 确定这个文件是否为一个能被虚拟机接收的 Class 文件。
Class 文件版本 :Class 文件的版本号,保证编译正常执行。
常量池 :常量池主要存放两大常量:字面量和符号引用。
访问标志 :标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口, 是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
当前类索引,父类索引 :类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之 外,所有的 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。
接口索引集合 :接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implents(如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。
字段表集合 :描述接口或类中声明的变量、字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
方法表集合 :类中的方法。
属性表集合 : 在 Class 文件,字段表,方法表中都可以携带自己的属性表集合。
说一下 JVM 调优的工具?
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有: MAT(Memory AnalyzerTool)、GChisto。
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的 java监控和管理控制台,用于对JVM中内存, 线程和类等的监控。
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富 的Javaheap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。
GChisto,一款专业分析gc日志的工具。
JVM调优命令有哪些?
jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出 虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap,JVM Memory Map命令用于生成heap dump文件 jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat 内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看 jstack,用于生成java虚拟机当前时刻的线程快照。
jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
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 In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节 码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高 Java 应用的性能。
程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执 行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有 执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
如何判断一个常量是废弃常量 ?
运行时常量池主要回收的是废弃的常量。假如在常量池中存在字符串 "abc",如果当前没有任 何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存 回收的话而且有必要的话,"abc" 就会被系统清理出常量池。
1.jvm基本概念
JVM 是可运行 Java 代码的假想计算机 ,包括
- 一套字节码指令集
- 一组寄存器
- 一个栈
- 一个垃圾回收堆
- 一个存储方法域。
JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
2.jvm运行过程
我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。 也就是如下:
- Java 源文件—->编译器—->字节码文件
- 字节码文件—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。
3.线程
这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。
Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。 Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时, 13/04/2018 Page 21 of 283会释放原生线程和 Java 线程的所有资源
。
Hotspot JVM 后台运行的系统线程主要有下面几个:
虚拟机线程(VM thread) | 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-theworld 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。 |
---|---|
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。 |
GC 线程 | 这些线程支持 JVM 中不同的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
4.JVM 内存区域
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内
, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
线程共享区域随虚拟机的启动/关闭而创建/销毁。 直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
5.程序计数器(线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
6.虚拟机栈(线程私有)
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
7.本地方法区(线程私有)
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
8.堆(Heap-线程共享)-运行时数据区
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
9.方法区/永久代(线程共享)
即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。
运行时常量池
(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。
10.JVM 运行时内存
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、FromSurvivor 区和 To Survivor 区)和老年代。
11.新生代
新生代是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
- Eden 区 Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
- ServivorFrom 上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
- ServivorTo 保留了一次 MinorGC 过程中的幸存者。
- MinorGC 的过程(复制->清空->互换)
MinorGC 采用复制算法。
- eden、servicorFrom 复制到 ServicorTo,年龄+1 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
- 清空 eden、servicorFrom 然后,清空 Eden 和 ServicorFrom 中的对象;
- ServicorTo 和 ServicorFrom 互换 最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。
12.老年代
主要存放应用程序中生命周期长的内存对象。 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
13.永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
14.JAVA8 与元数据
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
15. 如何确定垃圾
- 引用计数法 在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
- 可达性分析 为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
16.标记清除算法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。而标记清除算法(Mark-Sweep)最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
17.复制算法(copying)
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
18.标记整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
19.分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
21.老年代与标记复制算法
而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
- JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
- 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
- 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。
- 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
- 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
- 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。**默认情况下年龄到达 15 的对象会被移到老生代中
22.JAVA 四中引用类型
- 强引用 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
- 软引用 软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
- 弱引用 弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
- 虚引用 虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
23.分代收集算法
当前主流 VM 垃圾收集都采用”分代收集”(GenerationalCollection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代
、老年代
、永久代
,这样就可以根据各年代特点分别采用最适当的 GC 算法
- 在新生代-复制算法
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用
复制算法
, 只需要付出少量存活对象的复制成本就可以完成收集. - 在老年代-标记整理算法
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“
标记—清理
”或“标记—整理
”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.
24.分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
25.GC 垃圾收集器
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器,JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
27.ParNew 垃圾收集器(Serial+多线程)
ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃 圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程。 ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。【Parallel:平行的】 ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。
28.Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。**自适应调节策略也是ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
29.Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。 在 Server 模式下,主要有两个用途:
- 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
- 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图: 新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。新生代 ParallelScavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:
30.Parallel Old 收集器(多线程标记整理算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。在 JDK1.6 之前,新生代使用ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。 新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:
31.CMS 收集器(多线程标记清除算法)
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
-
初始标记 只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
-
并发标记 进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
-
重新标记 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
-
并发清除 清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
32.G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
26.Serial 垃圾收集器(单线程、复制算法)
Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。
20.新生代与复制算法
目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
44.JVM 类加载机制
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。
45.加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
46.验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
47.准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
public static int v = 8080;
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是
程序被编译后,存放于类构造器<client>方法之中
。
但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080。
48.解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
- CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等类型的常量。
49.符号引用
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
50.直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
51.初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
52.类构造器
初始化阶段是执行类构造器<client>方法的过程
。<client>
方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<client>
方法执行之前,父类的<client>
方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>
()方法。
注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取 Class 对象,不会触发类的初始化。
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
- 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
53.类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提供了 3 种类加载器:
1. 启动类加载器(Bootstrap ClassLoader)
- 负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2. 扩展类加载器(Extension ClassLoader)
- 负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3. 应用程序类加载器(Application ClassLoader):
- 负责加载用户路径(classpath)上的类库。 JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
54.双亲委派
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
55.OSGI(动态模型系统)
OSGi(Open Service Gateway Initiative),是面向 Java 的动态模型系统,是 Java 动态化模块化系统的一系列规范。
-
动态改变构造 OSGi 服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGi 技术提供一种面向服务的架构,它能使这些组件动态地发现对方。
-
模块化编程与热插拔 OSGi 旨在为实现 Java 程序的模块化编程提供基础条件,基于 OSGi 的程序很可能可以实现模块级的热插拔功能,当程序升级更新时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。 OSGi 描绘了一个很美好的模块化开发目标,而且定义了实现这个目标的所需要服务与架构,同时也有成熟的框架进行实现支持。但并非所有的应用都适合采用 OSGi 作为基础架构,它在提供强大功能同时,也引入了额外的复杂度,因为它不遵守了类加载的双亲委托模型。