一Java的一些关键概念及其关系JDK:Java 开发工具包(Java Development Kit)JRE:Java 运行时环境(Java Runtime Environment)JVM:Java虚拟机二、JVM运行流程1Java源代码编译成class字节码⽂件由类加载系统装载到运⾏时数据区2)运⾏时数据区把字节码加载到内存3)执⾏引擎负责将字节码翻译为底层系统指令。JVM跨平台原理不同操作系统上有相应的JVM,将.class文件转化为能在自身CPU上执行的指令本质一次编译各处运行C语言没有虚拟机所以在不同机器上需要分别进行编译扩展物理计算机层面的架构具体可阅读《深入理解计算机系统》的第三章三、JVM内存结构即上图的运行时数据区,运⾏时数据区⼜分为⽅法区、堆、虚拟机栈、本地⽅法栈、程序计数器线程私有的有程序计数器、虚拟机栈、本地⽅法栈线程共享的有堆、⽅法区1.程序计数器记录当前线程正在执⾏的字节码指令的地址物理上程序计数器是使⽤“寄存器”完成的。JVM 里的程序计数器是逻辑概念是 JVM 为每个线程维护的「字节码执行位置」最终会被 JVM 翻译成物理 CPU 的机器指令地址写入物理 PC 寄存器2.虚拟机栈也叫线程栈每个线程运⾏时所需要的内存⼀个栈是由多个栈帧组成栈帧对应着每次⽅法调⽤时所占有的内存存储的内容是⽅法参数、⽅法内局部变量、⽅法返回地址⼀个线程每时刻只能有⼀个活动栈帧对应当前正在执⾏的⽅法垃圾回收不涉及到 栈内存⽅法递归过多 会导致 java.lang.StackOverflowError 栈内存溢出3.本地⽅法栈本地⽅法接⼝运⾏时所需要的内存空间以 native 修饰的⽅法就是本地⽅法本地⽅法不是⽤Java写的没有⽅法实现的只有⼀个接⼝供Java调⽤⽽是⽤C或C编写因为 Java 有些时候不能直接和操作系统底层打交道因此Java通过接⼝间接调⽤C或C编写的本地⽅法来与操作系统底层的API打交道。即Java通过本地⽅法调⽤操作系统底层功能。4.堆线程共享的区域保存对象实例逻辑组成年轻代(⽣命周期短的对象) ⽼年代(⽣命周期⻓的对象)JDK1.7中堆中存在 ⽅法区的实现永久代 JDK1.8把⽅法区的实现从堆移到本地内存且叫做元空间5.⽅法区线程共享的区域存储的是类信息、静态变量、常量、编译后的代码、运⾏时常量池JDK7⽅法区的实现叫永久代占⽤的是堆的内存空间⼤⼩固定JDK8⽅法区的实现叫元空间占⽤的是本地内存的空间⼤⼩⾃动调整四、创建对象一定要分配在堆里吗对象分配流程逃逸分析和栈上分配1、逃逸分析JVM通过分析对象的动态作⽤域判断对象是否可能逃逸出当前⽅法或线程未逃逸对象仅在⽅法内部使⽤不会被外部⽅法或线程访问⽅法逃逸对象作为返回值或参数传递到其他⽅法线程逃逸对象被其他线程访问如赋值给静态变量栈上分配如果对象未逃逸JVM可能将其分配在栈帧中跟随⽅法调⽤结束⾃动销毁避免堆内存分配减少GC压⼒。2.如果本线程的TLAB区域未满则优先将对象分配到TLAB3.对象分配在堆中TLAB定义TLAB 是 JVM 的⼀种内存分配的优化机制。⽤于提⾼多线程环境下内存分配的效率。对象的分配流程TLAB问题引⼊多线程同时new对象时JVM为其分配内存为了防⽌内存重复分配需要加⼊互斥 锁降低了内存分配的速度。解决⽅案所以引⼊了TLAB的概念Threal Local Allocate Buffer是每个线程在Eden区专属的⼀块区域当线程的TLAB区域未满时可以直接在其中分配内存不需要加锁提⾼内存分配速度。当⼀个线程创建的时候JVM会为这个线程分配⼀个固定⼤⼩的TLAB空间TLAB 仅优化内存分配TLAB 的线程安全仅体现在内存分配过程中与对象本身的线程安全性⽆关五、垃圾回收GC)1.什么是垃圾早期我们使⽤引⽤计数法来判断对象是否是垃圾但发现它存在循环引⽤导致内存泄漏的问题所以我们现在都是采⽤根可达性算法去判断对象是否是垃圾。2.垃圾回收算法垃圾回收算法有三种分别是标记清除、标记整理、复制。标记清除算法 优点是垃圾回收速度快缺点是会产⽣内存碎⽚造成内存浪费和溢出标记整理算法 优点是不会产⽣内存碎⽚缺点是效率较低整理涉及到对象的移动较复杂复制算法 优点是不会产⽣内存碎⽚且较为⾼效缺点是占⽤双倍的内存空间内存空间开销⼤3.分代垃圾回收但我们⼀般不单独使⽤某⼀种垃圾回收算法由于对象的⽣命周期是不同的有的产⽣后很快可以回收有的会存活很久对象的差异化决定了我们要差异化管理它们所以我们通常把堆分成年轻代和⽼年代年轻代中存储的是朝⽣夕死的对象因此它们是会频繁发⽣GC回收的所以年轻代⼀般采⽤的是复制算法⽽⽼年代中存储的是⽣命周期很⻓的对象因此它们很久才会发⽣GC回收⼀次所以⽼年代⼀般采⽤标记整理算法。4.垃圾回收器例如 CMSCMS 是针对⽼年代的、追求响应时间优先的垃圾回收器主要在两⽅⾯实现第⼀CMS采⽤的是标记清除算法优点就是速度快第⼆在CMS的垃圾回收过程中在⼀些阶段并发标记和并发清理中是允许⽤户线程和垃圾回收线程同时⼯作的通过这两点来达到响应时间优先。⼜例如G1G1是⼀个全年代的、可设置每次垃圾回收STW的暂停时间⽬标的垃圾回收器例如⼀次STW的时间不能超过200ms它的实现原理要从结构讲起G1把堆内存划分为若⼲个⼤⼩相同的区每个区都会产⽣垃圾但在垃圾回收时可以选择性的回收垃圾较多的区域垃圾较少的区域暂时不回收这样效益较⾼短时间内回收了尽可能多的垃圾尽可能达到STW的暂停时间⽬标。5.GC 调优我们有时候根据场景可以通过替换垃圾回收器来进⾏GC调优JDK8的默认垃圾回收器是Parallel GC并⾏垃圾回收器例如系统要求低延迟低停顿我们可以更换垃圾回收器为CMS如果我们的系统有着超⼤堆内存空间且注重可预测停顿STW时间的话可以更换垃圾回收器为G1。根据系统特性来进⾏选择。另外像Java本身他也⼀直在优化GC例如JDK7到JDK8的⼀个变化是把⽅法区的实现从堆内存移到了本地内存是因为⽅法区存储的是类的元信息⽽随着动态加载类的技术发展我们运⾏时也会加载很多类所以⽅法区所占有的内存⼤⼩变得不可预知为了防⽌它对堆内存的GC影响所以把它从堆中移动出来。JDK7永久代⼤⼩固定JDK8元空间⼤⼩⾃动调整还有⽐如逃逸分析和栈上分配也是为了减轻GC压⼒六、垃圾回收器这里主要说一下CMS和G11、CMS:设计⽬标响应时间优先减少 STW 时间适⽤于要求低延迟、低停顿的系统。CMS 回收的详细过程为了达到响应时间优先的⽬的即STW时间短如何实现并发标记阶段和并发清理阶段都是垃圾回收线程和⽤户线程同时⼯作的CMS 的垃圾回收算法是标记清除算法速度快CMS 的弊端1.内存碎⽚问题原因标记-清除算法不整理内存⻓期运⾏后⽼年代会积累内存碎⽚影响即使⽼年代总空间⾜够⼤对象分配失败触发 Full GC 2.浮动垃圾问题定义在并发清理阶段中⽤户线程可能继续产⽣垃圾即“浮动垃圾”影响在并发清理阶段中若浮动垃圾过多导致⽼年代空间不⾜并发模式失败触发Full GC2、G1堆划分G1 不再像以前的垃圾回收器 对于堆内存分为两块 新⽣代和⽼年代G1 是⼀个全年代的垃圾回收器把整个堆内存划分为若⼲个⼤⼩相等的区域RegionRegion 类型动态分配为 Eden、Survivor、Old 或 Humongous存放⼤对象每个类型的Region 数量动态变化分区取代分代可以⽀持更灵活的回收策略算法驱动设计不同的回收算法需要不同的内存结构⽀持。G1 的⼀个特点是 可以设置每次垃圾回收STW的暂停时间⽬标⽐如STW的时间不能超过200ms实现原理这要结合 G1 把堆内存划分为若⼲个⼤⼩相同的区来看每个区都会产⽣垃圾当 G1进⾏回收时可以仅仅选择回收部分垃圾较多的区域垃圾较少的区域暂时不回收第⼀范围缩⼩了节省了时间第⼆也回收了相对多的垃圾由此去使得STW的时间接近设定的暂停时间⽬标。实现机制G1 回收的详细过程1.初始标记需 STW标记 GC Roots 直接关联的对象2.并发标记与⽤户线程并发执⾏从初始标记阶段标记的对象出发遍历整个对象图标记所有可达的对象3.最终标记需 STW由于并发标记阶段允许⽤户线程⼯作可能会有些对象的引⽤关系变化导致漏标(对象消失)。重新标记阶段需要再次STW处理这些在并发标记期间变化的对象确保标记的准确性。4. 筛选回收需 STW制定回收计划选择新⽣代⽼年代⼤对象中的部分Region进⾏垃圾回收复制存活对象到空Region清空原 Region达到暂停时间的⽬标G1 适⽤于拥有超⼤堆内存、⾼吞吐量、低延迟、可预测暂停时间⽬标的系统七、类加载器1、类加载器的作⽤就是将 字节码⽂件 加载到 JVM 内存中2、常见的类加载器Bootstrap ClassLoader 启动类加载器加载 JAVA_HOME/jre/lib 下的类⽆法直接访问由C/C实现Extension ClassLoader 扩展类加载器加载 JAVA_HOME/jre/lib/ext 下的类上级为 BootstrapApplication ClassLoader 应⽤程序类加载器加载 classpath ⽬录就是我们平时代码的⽬录下的类上级为 Extension⾃定义类加载器可以实现⾃定义的类加载规则通过⾃定义类加载器可以绕过/破坏双亲委派例如优先⾃⼰加载类实现类隔离或多版本共存。上级为 Application3、双亲委派指明了 加载类时 类加载器的⾏为双亲委派模型的⼯作过程如果⼀个类加载器收到了类加载的请求它⾸先不会⾃⼰去尝试加载这个类⽽是把这个请求委派给⽗类加载器去完成每⼀个层次的类加载器都是如此因此所有的加载请求最终都应该传送到顶层的启动类加载器中只有当⽗加载器反馈⾃⼰⽆法完成这个加载请求(它的搜索范围中没有找到所需的类)时⼦加载器才会尝试⾃⼰去加载。注意类加载器的⽗⼦关系不是以继承的⽅式实现⽽是组合底层源码使用双亲委派的好处1类的唯⼀性通过双亲委派机制可以避免某⼀个类被重复加载当⽗加载器已经加载后则⽆需重复加载2)类的安全性防⽌⽤户⾃定义类覆盖核⼼类保证核⼼类不会被修改破坏双亲委派只需要⾃定义类加载器并且重写loadClass⽅法且内部逻辑不遵循双亲委派双亲委派模型对于保证Java程序的稳定很重要但实现却很简单实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()⽅法之中eg:Tomcat破坏双亲委派八、JVM调优1、调优目的减少STW2、如何调整我们可以调整堆内存的初始⼤⼩、堆内存的最⼤⼤⼩年轻代的初始⼤⼩、年轻代的最⼤⼤⼩年轻代与⽼年代之间的⽐例默认是12年轻代中 Eden 区 与 Survivor 区的⽐例默认是81 : 1元空间的初始⼤⼩、元空间的最⼤⼤⼩更换垃圾收集器3、调优案例服务环境ParNew CMS JDK8C端核⼼业务每个接⼝的请求耗时⼀般有限制在例如100ms以内C端核⼼业务在⾼峰期服务器发⽣ FullGCFullGC的耗时甚⾄可以达到1s导致部分请求超时报错影响⽤户体验原因分析针对⽼年代区域CMS使⽤标记清除算法意味着⽼年代区域随着应⽤的运⾏会变得碎⽚化碎⽚过多会影响对象的分配虽然⽼年代还有很⼤的剩余空间但是没有连续的空间来分配对象⻓期如此最终会导致FullGC的发⽣。优化策略业务低峰期(例如凌晨四点)显式触发FullGC System.gc()CMS触发FullGC就会退化为单线程的SerialOld垃圾回收器它会采⽤标记整理算法进⾏回收可以整理堆内存优化掉内存碎⽚的情况降低业务⾼峰期发⽣FullGC的概率。(这是⼤⼚真实案例)优化效果业务⾼峰期基本没有出现FullGC虽然不能彻底避免FullGC但从效果上看⾮常有⽤总结复盘C端核⼼业务为了追求响应时间很短所以采⽤了CMS垃圾回收器标记清除算法快但也引⼊了内存碎⽚的问题结合业务有⾼峰期和低峰期所以在低峰期把内存碎⽚整理好尽量保证⾼峰期的正常运⾏。