魔力百科网站做料理视频,做网站手机端需要pc端的源代码吗,优秀网页欣赏,google国外入口1. 什么是JVM内存结构#xff1f; jvm将虚拟机分为5大区域#xff0c;程序计数器、虚拟机栈、本地方法栈、java堆、方法区#xff1b;
程序计数器#xff1a;线程私有的#xff0c;是一块很小的内存空间#xff0c;作为当前线程的行号指示器#xff0c;用于记录当前虚拟…1. 什么是JVM内存结构 jvm将虚拟机分为5大区域程序计数器、虚拟机栈、本地方法栈、java堆、方法区
程序计数器线程私有的是一块很小的内存空间作为当前线程的行号指示器用于记录当前虚拟机正在执行的线程指令地址虚拟机栈线程私有的每个方法执行的时候都会创建一个栈帧用于存储局部变量表、操作数、动态链接和方法返回等信息当线程请求的栈深度超过了虚拟机允许的最大深度时就会抛出StackOverFlowError本地方法栈线程私有的保存的是native方法的信息当一个jvm创建的线程调用native方法后jvm不会在虚拟机栈中为该线程创建栈帧而是简单的动态链接并直接调用该方法堆java堆是所有线程共享的一块内存几乎所有对象的实例和数组都要在堆上分配内存因此该区域经常发生垃圾回收的操作堆里主要包含老年代和年轻代方法区存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代在jdk1.8中不存在方法区了被元数据区替代了原方法区被分成两部分1加载的类信息2运行时常量池加载的类信息被保存在元数据区中运行时常量池保存在堆中
2. 什么是JVM内存模型
Java 内存模型下文简称 JMM就是在底层处理器内存模型的基础上定义自己的多线程语义。它明确指定了一组排序规则来保证线程间的可见性。
这一组规则被称为 Happens-Before, JMM 规定要想保证 B 操作能够看到 A 操作的结果无论它们是否在同一个线程那么 A 和 B 之间必须满足 Happens-Before 关系
单线程规则一个线程中的每个动作都 happens-before 该线程中后续的每个动作监视器锁定规则监听器的解锁动作 happens-before 后续对这个监听器的锁定动作volatile 变量规则对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作线程 start 规则线程 start() 方法的执行 happens-before 一个启动线程内的任意动作线程 join 规则一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前传递性如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
怎么理解 happens-before 呢如果按字面意思比如第二个规则线程不管是不是同一个的解锁动作发生在锁定之前这明显不对。happens-before 也是为了保证可见性比如那个解锁和加锁的动作可以这样理解线程1释放锁退出同步块线程2加锁进入同步块那么线程2就能看见线程1对共享对象修改的结果。 Java 提供了几种语言结构包括 volatile, final 和 synchronized, 它们旨在帮助程序员向编译器描述程序的并发要求其中
volatile - 保证可见性和有序性synchronized - 保证可见性和有序性; 通过**管程Monitor*保证一组动作的*原子性final - 通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序保证可见性如果 this 引用逃逸就不好说可见性了
编译器在遇到这些关键字时会插入相应的内存屏障保证语义的正确性。
有一点需要注意的是synchronized 不保证同步块内的代码禁止重排序因为它通过锁保证同一时刻只有一个线程访问同步块或临界区也就是说同步块的代码只需满足 as-if-serial 语义 - 只要单线程的执行结果不改变可以进行重排序。
所以说Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性另外还确保正确同步的 Java 代码可以在不同体系结构的处理器上正确运行。
3. heap 和stack 有什么区别
1申请方式
stack:由系统自动分配。例如声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间
heap:需要程序员自己申请并指明大小在 c 中 malloc 函数对于Java 需要手动 new Object()的形式开辟
2申请后系统的响应
stack只要栈的剩余空间大于所申请空间系统将为程序提供内存否则将报异常提示栈溢出。
heap首先应该知道操作系统有一个记录空闲内存地址的链表当系统收到程序的申请时会遍历该链表寻找第一个空间大于所申请空间的堆结点然后将该结点从空闲结点链表中删除并将该结点的空间分配给程序。另外由于找到的堆结点的大小不一定正好等于申请的大小系统会自动的将多余的那部分重新放入空闲链表中。
3申请大小的限制
stack栈是向低地址扩展的数据结构是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的在 WINDOWS 下栈的大小是 2M默认值也取决于虚拟内存的大小如果申请的空间超过栈的剩余空间时将提示 overflow。因此能从栈获得的空间较小。
heap堆是向高地址扩展的数据结构是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的 自然是不连续的而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见 堆获得的空间比较灵活也比较大。
4申请效率的比较
stack由系统自动分配速度较快。但程序员是无法控制的。
heap由 new 分配的内存一般速度比较慢而且容易产生内存碎片,不过用起来最方便。
5heap和stack中的存储内容
stack在函数调用时第一个进栈的是主函数中后的下一条指令函数调用语句的下一条可执行语句的地址 然后是函数的各个参数在大多数的 C 编译器中参数是由右往左入栈的然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后局部变量先出栈然后是参数最后栈顶指针指向最开始存的地址也就是主函数中的下一条指令程序由该点继续运行。
heap一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
4. 什么情况下会发生栈内存溢出
栈是线程私有的栈的生命周期和线程一样每个方法在执行的时候就会创建一个栈帧它包含局部变量表、操作数栈、动态链接、方法出口等信息局部变量表又包括基本数据类型和对象的引用当线程请求的栈深度超过了虚拟机允许的最大深度时会抛出StackOverFlowError异常方法递归调用肯可能会出现该问题调整参数-xss去调整jvm栈的大小
5. 谈谈对 OOM 的认识如何排查 OOM 的问题
除了程序计数器其他内存区域都有 OOM 的风险。
栈一般经常会发生 StackOverflowError比如 32 位的 windows 系统单进程限制 2G 内存无限创建线程就会发生栈的 OOMJava 8 常量池移到堆中溢出会出 java.lang.OutOfMemoryError: Java heap space设置最大元空间大小参数无效堆内存溢出报错同上这种比较好理解GC 之后无法在堆中申请内存创建对象就会报错方法区 OOM经常会遇到的是动态生成大量的类、jsp 等直接内存 OOM涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的方法
增加两个参数 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/tmp/heapdump.hprof当 OOM 发生时自动 dump 堆内存信息到指定目录同时 jstat 查看监控 JVM 的内存和 GC 情况先观察问题大概出在什么区域使用 MAT 工具载入到 dump 文件分析大对象的占用情况比如 HashMap 做缓存未清理时间长了就会内存溢出可以把改为弱引用 。
6. 谈谈 JVM 中的常量池
JVM常量池主要分为Class文件常量池、运行时常量池全局字符串常量池以及基本类型包装类对象常量池。
Class文件常量池。class文件是一组以字节为单位的二进制数据流在java代码的编译期间我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中其中就包括class文件常量池。运行时常量池运行时常量池相对于class常量池一大特征就是具有动态性java规范并不要求常量只能在运行时才产生也就是说运行时常量池的内容并不全部来自class常量池在运行时可以通过代码生成常量并将其放入运行时常量池中这种特性被用的最多的就是String.intern()。全局字符串常量池字符串常量池是JVM所维护的一个字符串实例的引用表在HotSpot VM中它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用底层C实现就是一个Hashtable。这些被维护的引用所指的字符串实例被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。 基本类型包装类对象常量池java中基本类型的包装类的大部分都实现了常量池技术这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池也即对象不负责创建和管理大于127的这些类的对象。
7. 如何判断一个对象是否存活
判断一个对象是否存活分为两种算法1引用计数法2可达性分析算法
引用计数法 给每一个对象设置一个引用计数器当有一个地方引用该对象的时候引用计数器就1引用失效时引用计数器就-1当引用计数器为0的时候就说明这个对象没有被引用也就是垃圾对象等待回收 缺点无法解决循环引用的问题当A引用BB也引用A的时候此时AB对象的引用都不为0此时也就无法垃圾回收所以一般主流虚拟机都不采用这个方法
可达性分析法 从一个被称为GC Roots的对象向下搜索如果一个对象到GC Roots没有任何引用链相连接时说明此对象不可用在java中可以作为GC Roots的对象有以下几种
虚拟机栈中引用的对象方法区类静态属性引用的变量方法区常量池引用的对象本地方法栈JNI引用的对象
但一个对象满足上述条件的时候不会马上被回收还需要进行两次标记第一次标记判断当前对象是否有finalize()方法并且该方法没有被执行过若不存在则标记为垃圾对象等待回收若有的话则进行第二次标记第二次标记将当前对象放入F-Queue队列并生成一个finalize线程去执行该方法虚拟机不保证该方法一定会被执行这是因为如果线程执行缓慢或进入了死锁会导致回收系统的崩溃如果执行了finalize方法之后仍然没有与GC Roots有直接或者间接的引用则该对象会被回收
8. 强引用、软引用、弱引用、虚引用是什么有什么区别
强引用就是普通的对象引用关系如 String s new String(ConstXiong)软引用用于维护一些可有可无的对象。只有在内存不足时系统则会回收软引用对象如果回收了软引用对象之后仍然没有足够的内存才会抛出内存溢出异常。SoftReference 实现弱引用相比软引用来说要更加无用一些它拥有更短的生命周期当 JVM 进行垃圾回收时无论内存是否充足都会回收被弱引用关联的对象。WeakReference 实现虚引用是一种形同虚设的引用在现实场景中用的不是很多它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现
9. 被引用的对象就一定能存活吗 不一定看 Reference 类型弱引用在 GC 时会被回收软引用在内存不足的时候即 OOM 前会被回收但如果没有在 Reference Chain 中的对象就一定会被回收。 10. Java中的垃圾回收算法有哪些
java中有四种垃圾回收算法分别是标记清除法、标记整理法、复制算法、分代收集算法 标记清除法 第一步利用可达性去遍历内存把存活对象和垃圾对象进行标记 第二步在遍历一遍将所有标记的对象回收掉 特点效率不行标记和清除的效率都不高标记和清除后会产生大量的不连续的空间分片可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC 标记整理法 第一步利用可达性去遍历内存把存活对象和垃圾对象进行标记 第二步将所有的存活的对象向一段移动将端边界以外的对象都回收掉 特点适用于存活对象多垃圾少的情况需要整理的过程无空间碎片产生 复制算法 将内存按照容量大小分为大小相等的两块每次只使用一块当一块使用完了就将还存活的对象移到另一块上然后在把使用过的内存空间移除 特点不会产生空间碎片内存使用率极低
分代收集算法 根据内存对象的存活周期不同将内存划分成几块java虚拟机一般将内存分成新生代和老生代在新生代中有大量对象死去和少量对象存活所以采用复制算法只需要付出少量存活对象的复制成本就可以完成收集老年代中因为对象的存活率极高没有额外的空间对他进行分配担保所以采用标记清理或者标记整理算法进行回收
对比 11. 有哪几种垃圾回收器各自的优缺点是什么
垃圾回收器主要分为以下几种Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1 Serial:单线程的收集器收集垃圾时必须stop the world使用复制算法。它的最大特点是在进行垃圾回收时需要对所有正在执行的线程暂停stop the world对于有些应用是难以接受的但是如果应用的实时性要求不是那么高只要停顿的时间控制在N毫秒之内大多数应用还是可以接受的是client级别的默认GC方式。 ParNew:Serial收集器的多线程版本也需要stop the world复制算 Parallel Scavenge:新生代收集器复制算法的收集器并发的多线程收集器目标是达到一个可控的吞吐量和ParNew的最大区别是GC自动调节策略虚拟机会根据系统的运行状态收集性能监控信息动态设置这些参数以提供最优停顿时间和最高的吞吐量 Serial Old:Serial收集器的老年代版本单线程收集器使用标记整理算法。 Parallel Old是Parallel Scavenge收集器的老年代版本使用多线程标记-整理算法。 CMS:是一种以获得最短回收停顿时间为目标的收集器标记清除算法运作过程初始标记并发标记重新标记并发清除收集结束会产生大量空间碎片 G1:标记整理算法实现运作流程主要包括以下初始标记并发标记最终标记筛选回收。不会产生空间碎片可以精确地控制停顿G1将整个堆分为大小相等的多个Region区域G1跟踪每个区域的垃圾大小在后台维护一个优先级列表每次根据允许的收集时间优先回收价值最大的区域已达到在有限时间内获取尽可能高的回收效率
垃圾回收器间的配合使用图 各个垃圾回收器对比 12. 详细说一下CMS的回收过程CMS的问题是什么
CMS(Concurrent Mark Sweep并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器追求低停顿它在垃圾收集时使得用户线程和 GC 线程并发执行因此在垃圾收集过程中用户也不会感到明显的卡顿。
从名字就可以知道CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步 初始标记 CMS initial mark)主要是标记 GC Root 开始的下级注仅下一级对象这个过程会 STW但是跟 GC Root 直接关联的下级对象不会很多因此这个过程其实很快。 并发标记 (CMS concurrent mark)根据上一步的结果继续向下标识所有关联的对象直到这条链上的最尽头。这个过程是多线程的虽然耗时理论上会比较长但是其它工作线程并不会阻塞没有 STW。 重新标记CMS remark顾名思义就是要再标记一次。为啥还要再标记一次因为第 2 步并没有阻塞其它工作线程其它线程在标识过程中很有可能会产生新的垃圾。 并发清除CMS concurrent sweep清除阶段是清理删除掉标记阶段判断的已经死亡的对象由于不需要移动存活对象所以这个阶段也是可以与用户线程同时并发进行的。
CMS 的问题
1. 并发回收导致CPU资源紧张
在并发阶段它虽然不会导致用户线程停顿但却会因为占用了一部分线程而导致应用程序变慢降低程序总吞吐量。CMS默认启动的回收线程数是CPU核数 3/ 4当CPU核数不足四个时CMS对用户程序的影响就可能变得很大。
2. 无法清理浮动垃圾
在CMS的并发标记和并发清理阶段用户线程还在继续运行就还会伴随有新的垃圾对象不断产生但这一部分垃圾对象是出现在标记过程结束以后CMS无法在当次收集中处理掉它们只好留到下一次垃圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败Concurrent Mode Failure
由于在垃圾回收阶段用户线程还在并发运行那就还需要预留足够的内存空间提供给用户线程使用因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收必须预留一部分空间供并发回收时的程序运行使用。默认情况下当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收这个值可以通过 -XX**:** CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险要是CMS运行期间预留的内存无法满足程序分配新对象的需要就会出现一次“并发失败”Concurrent Mode Failure这时候虚拟机将不得不启动后备预案Stop The World临时启用 Serial Old 来重新进行老年代的垃圾回收这样一来停顿时间就很长了。
4.内存碎片问题
CMS是一款基于“标记-清除”算法实现的回收器这意味着回收结束时会有内存碎片产生。内存碎片过多时将会给大对象分配带来麻烦往往会出现老年代还有很多剩余空间但就是无法找到足够大的连续空间来分配当前对象而不得不提前触发一次 Full GC 的情况。
为了解决这个问题CMS收集器提供了一个 -XX**:UseCMSCompactAtFullCollection 开关参数默认开启用于在 Full GC 时开启内存碎片的合并整理过程由于这个内存整理必须移动存活对象是无法并发的这样停顿时间就会变长。还有另外一个参数 -XX:**CMSFullGCsBeforeCompaction这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后下一次进入 Full GC 前会先进行碎片整理默认值为0表示每次进入 Full GC 时都进行碎片整理。
13. 详细说一下G1的回收过程
G1Garbage First回收器采用面向局部收集的设计思路和基于Region的内存布局形式是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器取代了 Parallel Scavenge 加 Parallel Old 的默认组合而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器但从局部两个Region之间上看又是基于 标记-复制 算法实现的。
G1 回收过程G1 回收器的运作过程大致可分为四个步骤 初始标记会STW仅仅只是标记一下 GC Roots 能直接关联到的对象并且修改TAMS指针的值让下一阶段用户线程并发运行时能正确地在可用的Region中分配新对象。这个阶段需要停顿线程但耗时很短而且是借用进行Minor GC的时候同步完成的所以G1收集器在这个阶段实际并没有额外的停顿。 并发标记从 GC Roots 开始对堆中对象进行可达性分析递归扫描整个堆里的对象图找出要回收的对象这阶段耗时较长但可与用户程序并发执行。当对象图扫描完成以后还要重新处理在并发时有引用变动的对象。 最终标记会STW对用户线程做短暂的暂停处理并发阶段结束后仍有引用变动的对象。 清理阶段会STW更新Region的统计数据对各个Region的回收价值和成本进行排序根据用户所期望的停顿时间来制定回收计划可以自由选择任意多个Region构成回收集然后把决定回收的那一部分Region的存活对象复制到空的Region中再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动必须暂停用户线程由多条回收器线程并行完成的。
14. JVM中一次完整的GC是什么样子的
先描述一下Java堆内存划分。
在 Java 中堆被划分成两个不同的区域新生代 ( Young )、老年代 ( Old )新生代默认占总空间的 1/3老年代默认占 2/3。 新生代有 3 个分区Eden、To Survivor、From Survivor它们的默认占比是 8:1:1。
新生代的垃圾回收又称Minor GC后只有少量对象存活所以选用复制算法只需要少量的复制成本就可以完成回收。
老年代的垃圾回收又称Major GC通常使用“标记-清理”或“标记-整理”算法。 再描述它们之间转化流程 对象优先在Eden分配。当 eden 区没有足够空间进行分配时虚拟机将发起一次 Minor GC。 在 Eden 区执行了第一次 GC 之后存活的对象会被移动到其中一个 Survivor 分区 Eden 区再次 GC这时会采用复制算法将 Eden 和 from 区一起清理存活的对象会被复制到 to 区移动一次对象年龄加 1对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置默认为 15动态对象年龄判定Survivor 区相同年龄所有对象大小的总和 (Survivor 区内存大小 * 这个目标使用率)时大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定默认为 50%Survivor 区内存不足会发生担保分配超过指定大小的对象可以直接进入老年代。 大对象直接进入老年代大对象就是需要大量连续内存空间的对象比如字符串、数组为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 老年代满了而无法容纳更多的对象Minor GC 之后通常就会进行Full GCFull GC 清理整个内存堆 – 包括年轻代和老年代。
15. Minor GC 和 Full GC 有什么不同呢
Minor GC只收集新生代的GC。
Full GC: 收集整个堆包括 新生代老年代永久代(在 JDK 1.8及以后永久代被移除换为metaspace 元空间)等所有部分的模式。
**Minor GC触发条件**当Eden区满时触发Minor GC。
Full GC触发条件
通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大则不会触发Minor GC而是转为触发full GC。老年代空间不够分配新的内存或永久代空间不足但只是JDK1.7有的这也是用元空间来取代永久代的原因可以减少Full GC的频率减少GC负担提升其效率。由Eden区、From Space区向To Space区复制时对象大小大于To Space可用内存则把该对象转存到老年代且老年代的可用内存小于该对象大小。调用System.gc时系统建议执行Full GC但是不必然执行。
16. 介绍下空间分配担保原则
如果YougGC时新生代有大量对象存活下来而 survivor 区放不下了这时必须转移到老年代中但这时发现老年代也放不下这些对象了那怎么处理呢其实JVM有一个老年代空间分配担保机制来保证对象能够进入老年代。
在执行每次 YoungGC 之前JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下可能新生代 YoungGC 后所有对象都存活下来了而 survivor 区又放不下那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的那就有可能老年代空间不够放入新生代所有存活对象这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是否允许担保失败如果允许就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小如果大于将尝试进行一次YoungGC尽快这次YoungGC是有风险的。如果小于或者 -XX:HandlePromotionFailure 参数不允许担保失败这时就会进行一次 Full GC。
在允许担保失败并尝试进行YoungGC后可能会出现三种情况
① YoungGC后存活对象小于survivor大小此时存活对象进入survivor区中② YoungGC后存活对象大于survivor大小但是小于老年大可用空间大小此时直接进入老年代。③ YoungGC后存活对象大于survivor大小也大于老年大可用空间大小老年代也放不下这些对象了此时就会发生“Handle Promotion Failure”就触发了 Full GC。如果 Full GC后老年代还是没有足够的空间此时就会发生OOM内存溢出了。
通过下图来了解空间分配担保原则 17. 什么是类加载类加载的过程
虚拟机把描述类的数据加载到内存里面并对数据进行校验、解析和初始化最终变成可以被虚拟机直接使用的class对象
类的整个生命周期包括加载Loading、验证Verification、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接Linking。如图所示 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的类的加载过程必须按照这种顺序按部就班地开始而解析阶段则不一定它在某些情况下可以在初始化阶段之后再开始这是为了支持Java语言的运行时绑定也称为动态绑定或晚期绑定
类加载过程如下 加载加载分为三步 1、通过类的全限定性类名获取该类的二进制流 2、将该二进制流的静态存储结构转为方法区的运行时数据结构 3、在堆中为该类生成一个class对象 验证验证该class文件中的字节流信息复合虚拟机的要求不会威胁到jvm的安全 准备为class对象的静态变量分配内存初始化其初始值 解析该阶段主要完成符号引用转化成直接引用 初始化到了初始化阶段才开始执行类中定义的java代码初始化阶段是调用类构造器的过程
18. 什么是类加载器常见的类加载器有哪些
类加载器是指通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器类加载器分为以下四种 启动类加载器BootStrapClassLoader用来加载java核心类库无法被java程序直接引用 扩展类加载器Extension ClassLoader用来加载java的扩展库java的虚拟机实现会提供一个扩展库目录该类加载器在扩展库目录里面查找并加载java类 系统类加载器AppClassLoader它根据java的类路径来加载类一般来说java应用的类都是通过它来加载的 自定义类加载器由java语言实现继承自ClassLoader 19. 什么是双亲委派模型为什么需要双亲委派模型
当一个类加载器收到一个类加载的请求他首先不会尝试自己去加载而是将这个请求委派给父类加载器去加载只有父类加载器在自己的搜索范围类查找不到给类时子加载器才会尝试自己去加载该类
为了防止内存中出现多个相同的字节码因为如果没有双亲委派的话用户就可以自己定义一个java.lang.String类那么就无法保证类的唯一性。
补充那怎么打破双亲委派模型
自定义类加载器继承ClassLoader类重写loadClass方法和findClass方法。
20. 列举一些你知道的打破双亲委派机制的例子为什么要打破 JNDI 通过引入线程上下文类加载器可以在 Thread.setContextClassLoader 方法设置默认是应用程序类加载器来加载 SPI 的代码。有了线程上下文类加载器就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因是为了 JNDI 服务的类加载器是启动器类加载为了完成高级类加载器请求子类加载器即上文中的线程上下文加载器加载类。 Tomcat应用的类加载器优先自行加载应用目录下的 class并不是先委派给父加载器加载不了才委派给父加载器。 tomcat之所以造了一堆自己的classloader大致是出于下面三类目的 对于各个 webapp中的 class和 lib需要相互隔离不能出现一个应用中加载的类库会影响另一个应用的情况而对于许多应用需要有共享的lib以便不浪费资源。与 jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库以免其他恶意或无意的破坏热部署。tomcat类加载器如下图 OSGi实现模块化热部署为每个模块都自定义了类加载器需要更换模块时模块与类加载器一起更换。其类加载的过程中有平级的类加载器加载行为。打破的原因是为了实现模块热替换。 JDK 9Extension ClassLoader 被 Platform ClassLoader 取代当平台及应用程序类加载器收到类加载请求在委派给父加载器加载前要先判断该类是否能够归属到某一个系统模块中如果可以找到这样的归属关系就要优先委派给负责那个模块的加载器完成加载。打破的原因是为了添加模块化的特性。
21. 说一下 JVM 调优的命令
jpsJVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。jstatjstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。jmapjmap(JVM Memory Map)命令用于生成heap dump文件如果不使用这个命令还阔以使用-XX:HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件还阔以查询finalize执行队列、Java堆和永久代的详细信息如当前使用率、当前使用的是哪种收集器等。jhatjhat(JVM Heap Analysis Tool)命令是与jmap搭配使用用来分析jmap生成的dumpjhat内置了一个微型的HTTP/HTML服务器生成dump的分析结果后可以在浏览器中查看。在此要注意一般不会直接在服务器上进行分析因为jhat是一个耗时并且耗费硬件资源的过程一般把服务器生成的dump文件复制到本地或其他机器上进行分析。jstackjstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈就可以知道没有响应的线程到底在后台做什么事情或者等待什么资源。 如果java程序崩溃生成core文件jstack工具可以用来获得core文件的java stack和native stack的信息从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
22. Java对象创建过程
JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类类加载过程在后边讲为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”最终常用的办法“本地线程缓冲分配(TLAB)”将除对象头外的对象内存空间初始化为0对对象头进行必要设置 线上故障排查
1、硬件故障排查
如果一个实例发生了问题根据情况选择要不要着急去重启。如果出现的CPU、内存飙高或者日志里出现了OOM异常
第一步是隔离第二步是保留现场第三步才是问题排查。
隔离
就是把你的这台机器从请求列表里摘除比如把 nginx 相关的权重设成零。
现场保留
瞬时态和历史态 查看比如 CPU、系统内存等通过历史状态可以体现一个趋势性问题而这些信息的获取一般依靠监控系统的协作。
保留信息
1系统当前网络连接
ss -antp $DUMP_DIR/ss.dump 21使用 ss 命令而不是 netstat 的原因是因为 netstat 在网络连接非常多的情况下执行非常缓慢。
后续的处理可通过查看各种网络连接状态的梳理来排查 TIME_WAIT 或者 CLOSE_WAIT或者其他连接过高的问题非常有用。
2网络状态统计
netstat -s $DUMP_DIR/netstat-s.dump 21
它能够按照各个协议进行统计输出对把握当时整个网络状态有非常大的作用。
sar -n DEV 1 2 $DUMP_DIR/sar-traffic.dump 21
在一些速度非常高的模块上比如 Redis、Kafka就经常发生跑满网卡的情况。表现形式就是网络通信非常缓慢。
3进程资源
lsof -p $PID $DUMP_DIR/lsof-$PID.dump
通过查看进程能看到打开了哪些文件可以以进程的维度来查看整个资源的使用情况包括每条网络连接、每个打开的文件句柄。同时也可以很容易的看到连接到了哪些服务器、使用了哪些资源。这个命令在资源非常多的情况下输出稍慢请耐心等待。
4CPU 资源
mpstat $DUMP_DIR/mpstat.dump 21
vmstat 1 3 $DUMP_DIR/vmstat.dump 21
sar -p ALL $DUMP_DIR/sar-cpu.dump 21
uptime $DUMP_DIR/uptime.dump 21主要用于输出当前系统的 CPU 和负载便于事后排查。
5I/O 资源
iostat -x $DUMP_DIR/iostat.dump 21
一般以计算为主的服务节点I/O 资源会比较正常但有时也会发生问题比如日志输出过多或者磁盘问题等。此命令可以输出每块磁盘的基本性能信息用来排查 I/O 问题。在第 8 课时介绍的 GC 日志分磁盘问题就可以使用这个命令去发现。
6内存问题
free -h $DUMP_DIR/free.dump 21
free 命令能够大体展现操作系统的内存概况这是故障排查中一个非常重要的点比如 SWAP 影响了 GCSLAB 区挤占了 JVM 的内存。
7其他全局
ps -ef $DUMP_DIR/ps.dump 21
dmesg $DUMP_DIR/dmesg.dump 21
sysctl -a $DUMP_DIR/sysctl.dump 21
dmesg 是许多静悄悄死掉的服务留下的最后一点线索。当然ps 作为执行频率最高的一个命令由于内核的配置参数会对系统和 JVM 产生影响所以我们也输出了一份。
8进程快照最后的遗言jinfo
${JDK_BIN}jinfo $PID $DUMP_DIR/jinfo.dump 21
此命令将输出 Java 的基本进程信息包括环境变量和参数配置可以查看是否因为一些错误的配置造成了 JVM 问题。
9dump 堆信息
${JDK_BIN}jstat -gcutil $PID $DUMP_DIR/jstat-gcutil.dump 21
${JDK_BIN}jstat -gccapacity $PID $DUMP_DIR/jstat-gccapacity.dump 21
jstat 将输出当前的 gc 信息。一般基本能大体看出一个端倪如果不能可将借助 jmap 来进行分析。
10堆信息
${JDK_BIN}jmap $PID $DUMP_DIR/jmap.dump 21
${JDK_BIN}jmap -heap $PID $DUMP_DIR/jmap-heap.dump 21
${JDK_BIN}jmap -histo $PID $DUMP_DIR/jmap-histo.dump 21
${JDK_BIN}jmap -dump:formatb,file$DUMP_DIR/heap.bin $PID /dev/null 21
jmap 将会得到当前 Java 进程的 dump 信息。如上所示其实最有用的就是第 4 个命令但是前面三个能够让你初步对系统概况进行大体判断。因为第 4 个命令产生的文件一般都非常的大。而且需要下载下来导入 MAT 这样的工具进行深入分析才能获取结果。这是分析内存泄漏一个必经的过程。
11JVM 执行栈
${JDK_BIN}jstack $PID $DUMP_DIR/jstack.dump 21
jstack 将会获取当时的执行栈。一般会多次取值我们这里取一次即可。这些信息非常有用能够还原 Java 进程中的线程情况。
top -Hp $PID -b -n 1 -c $DUMP_DIR/top-$PID.dump 21
为了能够得到更加精细的信息我们使用 top 命令来获取进程中所有线程的 CPU 信息这样就可以看到资源到底耗费在什么地方了。
12高级替补
kill -3 $PID
有时候jstack 并不能够运行有很多原因比如 Java 进程几乎不响应了等之类的情况。我们会尝试向进程发送 kill -3 信号这个信号将会打印 jstack 的 trace 信息到日志文件中是 jstack 的一个替补方案。
gcore -o $DUMP_DIR/core $PID
对于 jmap 无法执行的问题也有替补那就是 GDB 组件中的 gcore将会生成一个 core 文件。我们可以使用如下的命令去生成 dump
${JDK_BIN}jhsdb jmap --exe ${JDK}java --core $DUMP_DIR/core --binaryheap
内存泄漏的现象
稍微提一下 jmap 命令它在 9 版本里被干掉了取而代之的是 jhsdb你可以像下面的命令一样使用。
jhsdb jmap --heap --pid 37340
jhsdb jmap --pid 37288
jhsdb jmap --histo --pid 37340
jhsdb jmap --binaryheap --pid 37340
一般内存溢出表现形式就是 Old 区的占用持续上升即使经过了多轮 GC 也没有明显改善。比如ThreadLocal里面的GC Roots内存泄漏的根本就是这些对象并没有切断和 GC Roots 的关系可通过一些工具能够看到它们的联系。
2、报表异常 | JVM调优
有一个报表系统频繁发生内存溢出在高峰期间使用时还会频繁的发生拒绝服务由于大多数使用者是管理员角色所以很快就反馈到研发这里。
业务场景是由于有些结果集的字段不是太全因此需要对结果集合进行循环并通过 HttpClient 调用其他服务的接口进行数据填充。使用 Guava 做了 JVM 内缓存但是响应时间依然很长。
初步排查JVM 的资源太少。接口 A 每次进行报表计算时都要涉及几百兆的内存而且在内存里驻留很长时间有些计算又非常耗 CPU特别的“吃”资源。而我们分配给 JVM 的内存只有 3 GB在多人访问这些接口的时候内存就不够用了进而发生了 OOM。在这种情况下没办法只有升级机器。把机器配置升级到 4C8G给 JVM 分配 6GB 的内存这样 OOM 问题就消失了。但随之而来的是频繁的 GC 问题和超长的 GC 时间平均 GC 时间竟然有 5 秒多。
进一步由于报表系统和高并发系统不太一样它的对象存活时长大得多并不能仅仅通过增加年轻代来解决而且如果增加了年轻代那么必然减少了老年代的大小由于 CMS 的碎片和浮动垃圾问题我们可用的空间就更少了。虽然服务能够满足目前的需求但还有一些不太确定的风险。
第一了解到程序中有很多缓存数据和静态统计数据为了减少 MinorGC 的次数通过分析 GC 日志打印的对象年龄分布把 MaxTenuringThreshold 参数调整到了 3特殊场景特殊的配置。这个参数是让年轻代的这些对象赶紧回到老年代去不要老呆在年轻代里。
第二我们的 GC 时间比较长就一块开了参数 CMSScavengeBeforeRemark使得在 CMS remark 前先执行一次 Minor GC 将新生代清掉。同时配合上个参数其效果还是比较好的一方面对象很快晋升到了老年代另一方面年轻代的对象在这种情况下是有限的在整个 MajorGC 中占的时间也有限。
第三由于缓存的使用有大量的弱引用拿一次长达 10 秒的 GC 来说。我们发现在 GC 日志里处理 weak refs 的时间较长达到了 4.5 秒。这里可以加入参数 ParallelRefProcEnabled 来并行处理Reference以加快处理速度缩短耗时。
优化之后效果不错但并不是特别明显。经过评估针对高峰时期的情况进行调研我们决定再次提升机器性能改用 8core16g 的机器。但是这带来另外一个问题。
高性能的机器带来了非常大的服务吞吐量通过 jstat 进行监控能够看到年轻代的分配速率明显提高但随之而来的 MinorGC 时长却变的不可控有时候会超过 1 秒。累积的请求造成了更加严重的后果。
这是由于堆空间明显加大造成的回收时间加长。为了获取较小的停顿时间我们在堆上改用了 G1 垃圾回收器把它的目标设定在 200ms。G1 是一款非常优秀的垃圾收集器不仅适合堆内存大的应用同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的 GC 暂停目标就能得到不错的性能。修改之后虽然 GC 更加频繁了一些但是停顿时间都比较小应用的运行较为平滑。
到目前为止也只是勉强顶住了已有的业务但是这时候领导层面又发力要求报表系统可以支持未来两年业务10到100倍的增长并保持其可用性但是这个“千疮百孔”的报表系统稍微一压测就宕机那如何应对十倍百倍的压力呢 ? 硬件即使可以做到动态扩容但是毕竟也有极限。
使用 MAT 分析堆快照发现很多地方可以通过代码优化那些占用内存特别多的对象
1、select * 全量排查只允许获取必须的数据
2、报表系统中cache实际的命中率并不高将Guava 的 Cache 引用级别改成弱引用WeakKeys
3、限制报表导入文件大小同时拆分用户超大范围查询导出请求。
每一步操作都使得JVM使用变得更加可用一系列优化以后机器相同压测数据性能提升了数倍。
3、大屏异常 | JUC调优
有些数据需要使用 HttpClient 来获取进行补全。提供数据的服务提供商有的响应时间可能会很长也有可能会造成服务整体的阻塞。 接口 A 通过 HttpClient 访问服务 2响应 100ms 后返回接口 B 访问服务 3耗时 2 秒。HttpClient 本身是有一个最大连接数限制的如果服务 3 迟迟不返回就会造成 HttpClient 的连接数达到上限概括来讲就是同一服务由于一个耗时非常长的接口进而引起了整体的服务不可用
这个时候通过 jstack 打印栈信息会发现大多数竟然阻塞在了接口 A 上而不是耗时更长的接口 B这个现象起初十分具有迷惑性不过经过分析后我们猜想其实是因为接口 A 的速度比较快在问题发生点进入了更多的请求它们全部都阻塞住的同时被打印出来了。
为了验证这个问题我搭建了一个demo 工程模拟了两个使用同一个 HttpClient 的接口。fast 接口用来访问百度很快就能返回slow 接口访问谷歌由于众所周知的原因会阻塞直到超时大约 10 s。 利用ab对两个接口进行压测同时使用 jstack 工具 dump 堆栈。首先使用 jps 命令找到进程号然后把结果重定向到文件可以参考 10271.jstack 文件。
过滤一下 nio 关键字可以查看 tomcat 相关的线程足足有 200 个这和 Spring Boot 默认的 maxThreads 个数不谋而合。更要命的是有大多数线程都处于 BLOCKED 状态说明线程等待资源超时。通过grep fast | wc -l 分析确实200个中有150个都是blocked的fast的进程。
问题找到了解决方式就顺利成章了。
1、fast和slow争抢连接资源通过线程池限流或者熔断处理
2、有时候slow的线程也不是一直slow所以就得加入监控
3、使用带countdownLaunch对线程的执行顺序逻辑进行控制
4、接口延迟 | SWAP调优
有一个关于服务的某个实例经常发生服务卡顿。由于服务的并发量是比较高的每多停顿 1 秒钟几万用户的请求就会感到延迟。
我们统计、类比了此服务其他实例的 CPU、内存、网络、I/O 资源区别并不是很大所以一度怀疑是机器硬件的问题。
接下来我们对比了节点的 GC 日志发现无论是 Minor GC还是 Major GC这个节点所花费的时间都比其他实例长得多。
通过仔细观察我们发现在 GC 发生的时候vmstat 的 si、so 飙升的非常严重这和其他实例有着明显的不同。
使用 free 命令再次确认发现 SWAP 分区使用的比例非常高引起的具体原因是什么呢
更详细的操作系统内存分布从 /proc/meminfo 文件中可以看到具体的逻辑内存块大小有多达 40 项的内存信息这些信息都可以通过遍历 /proc 目录的一些文件获取。我们注意到 slabtop 命令显示的有一些异常dentry目录高速缓冲占用非常高。
问题最终定位到是由于某个运维工程师删除日志时定时执行了一句命令
find / | grep xxx.log
他是想找一个叫做 要被删除 的日志文件看看在哪台服务器上结果这些老服务器由于文件太多扫描后这些文件信息都缓存到了 slab 区上。而服务器开了 swap操作系统发现物理内存占满后并没有立即释放 cache导致每次 GC 都要和硬盘打一次交道。
解决方式就是关闭 SWAP 分区。
swap 是很多性能场景的万恶之源建议禁用。在高并发 SWAP 绝对能让你体验到它魔鬼性的一面进程倒是死不了了但 GC 时间长的却让人无法忍受。
5、内存溢出 | Cache调优 有一次线上遇到故障重新启动后使用 jstat 命令发现 Old 区一直在增长。我使用 jmap 命令导出了一份线上堆栈然后使用 MAT 进行分析通过对 GC Roots 的分析发现了一个非常大的 HashMap 对象这个原本是其他同事做缓存用的但是做了一个无界缓存没有设置超时时间或者 LRU 策略在使用上又没有重写key类对象的hashcode和equals方法对象无法取出也直接造成了堆内存占用一直上升后来将这个缓存改成 guava 的 Cache并设置了弱引用故障就消失了。 关于文件处理器的应用在读取或者写入一些文件之后由于发生了一些异常close 方法又没有放在 finally 块里面造成了文件句柄的泄漏。由于文件处理十分频繁产生了严重的内存泄漏问题。 内存溢出是一个结果而内存泄漏是一个原因。内存溢出的原因有内存空间不足、配置错误等因素。一些错误的编程方式不再被使用的对象、没有被回收、没有及时切断与 GC Roots 的联系这就是内存泄漏。
举个例子有团队使用了 HashMap 做缓存但是并没有设置超时时间或者 LRU 策略造成了放入 Map 对象的数据越来越多而产生了内存泄漏。
再来看一个经常发生的内存泄漏的例子也是由于 HashMap 产生的。代码如下由于没有重写 Key 类的 hashCode 和 equals 方法造成了放入 HashMap 的所有对象都无法被取出来它们和外界失联了。所以下面的代码结果是 null。
//leak example
import java.util.HashMap;
import java.util.Map;
public class HashMapLeakDemo {public static class Key {String title;public Key(String title) {this.title title;}
}public static void main(String[] args) {MapKey, Integer map new HashMap();map.put(new Key(1), 1);map.put(new Key(2), 2);map.put(new Key(3), 2);Integer integer map.get(new Key(2));System.out.println(integer);}
}
即使提供了 equals 方法和 hashCode 方法也要非常小心尽量避免使用自定义的对象作为 Key。
再看一个例子关于文件处理器的应用在读取或者写入一些文件之后由于发生了一些异常close 方法又没有放在 finally 块里面造成了文件句柄的泄漏。由于文件处理十分频繁产生了严重的内存泄漏问题。
6、CPU飙高 | 死循环
我们有个线上应用单节点在运行一段时间后CPU 的使用会飙升一旦飙升一般怀疑某个业务逻辑的计算量太大或者是触发了死循环比如著名的 HashMap 高并发引起的死循环但排查到最后其实是 GC 的问题。
1使用 top 命令查找到使用 CPU 最多的某个进程记录它的 pid。使用 Shift P 快捷键可以按 CPU 的使用率进行排序。
top
2再次使用 top 命令加 -H 参数查看某个进程中使用 CPU 最多的某个线程记录线程的 ID。
top -Hp $pid
3使用 printf 函数将十进制的 tid 转化成十六进制。
printf %x $tid
4使用 jstack 命令查看 Java 进程的线程栈。
jstack $pid $pid.log
5使用 less 命令查看生成的文件并查找刚才转化的十六进制 tid找到发生问题的线程上下文。
less $pid.log
我们在 jstack 日志搜关键字DEAD以及中找到了 CPU 使用最多的几个线程id。
可以看到问题发生的根源是我们的堆已经满了但是又没有发生 OOM于是 GC 进程就一直在那里回收回收的效果又非常一般造成 CPU 升高应用假死。接下来的具体问题排查就需要把内存 dump 一份下来使用 MAT 等工具分析具体原因了。