参数含义

  • -Xmx代表最大堆容量,-Xms代表最小堆容量
  • -XX:PermSize , -XX:MaxPermSize表示最大方法区容量
  • -Xss设置栈容量

判断对象是否存活的算法

  • 引用计数算法
  • 给对象中添加一个引用计数器,每当有一个地方引用它是,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
  • 缺点:无法解决对象之间相互循环引用的问题。
  • 可达性分析算法(Java,C#)
  • 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的
  • Java中可作为GC Roots的对象:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • 不可用对象的"判刑"期
  • 不可达对象也并非"非死不可"的,他们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖fianlize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。
  • 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,单并不承诺会等待它运行结束,这么做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移除出"即将回收"的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
  • 判断类属于"无用的类"
  • 该类所有的示例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

  • 标记清除算法
  • 算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
  • 不足:一是效率问题,标记和清楚的两个过程的效率都不高;另一个问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。
  • 复制算法
  • 它将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。实现简单,运行高效。
  • 不足:将内存缩小为了原来的一半,未免太高了一点。
  • 改进版复制算法
  • 将堆内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被"浪费"。
  • 标记-整理算法
  • 标记过程与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  • 分代收集算法
  • 根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代每次垃圾收集时都会有大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记——清理"或者"标记——整理"算法来进行回收。

HotSpot算法实现

  • 要实现算法首先要枚举根节点,**问题一:**若程序一直在运行,引用就会一直变化,肯定没法枚举节点;问题二: 枚举时,若需要枚举区域所存引用过多,内存过大,逐个枚举肯定不可行;HotSpot在实际处理时,针对问题一,采用准确式GC,将执行系统停顿下来进行执行上下文和全局引用位置的检查;针对问题二,使用一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举。问题四: 在何时中断所有线程?设置安全点,只在安全点才停下;问题五: 同一时刻所有线程很有可能不会全部都正好到达安全点,怎么处理?采用主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的。问题六: 若进行GC时,未执行的程序(没有分配CPU时间,例如处于Sleep状态或者Blocked状态)该怎么办?提供安全区域进行解决,安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。

内存分配与回收策略

  • 新生代GC(Minor GC)与老年代GC(Major GC / Full GC)的区别:
  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代 GC(Major GC / FullGC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC (但非绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行Major GC 的策略选择过程)。Major GC的速度一般会比MinorGC慢10倍以上
  • 对象优先在Eden分配,大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGc。大对象直接进入老年代,所谓的大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(比大对象更糟的是遇到一群"朝生夕灭"的"短命"大对象,写程序的时候应该避免)。长期存活的对象将进入老年代,既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能别Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor区中每"熬过"一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了阈值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等待MaxTenuringThreshold中要求的年龄。空间分配担保,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

虚拟机类加载机制

类加载的时机
  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。
  • 虚拟机规范严格规定了有且只有5中情况必须立即对类进行"初始化"(而加载,验证,准备自然需要在此之前开始),这5中情景中的行为称为对一个类进行主动引用:
  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle示例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
  • 除了上述的5种主动引用外,所有引用类的方式都不会触发初始化,称为被动引用。
  • 通过子类引用父类的静态字段,不会导致子类初始化。
  • 通过数组定义来引用类,不会触发此类的初始化。
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载器
  • 启动类加载器(Bootstrap ClassLoader)
  • 负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存汇总。启动类加载器无法被Java程序直接引用。
  • 扩展类加载器(Extension ClassLoader)
  • 负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)
  • 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
  • 关系:自定义类加载器 --> 应用程序类加载器 --> 扩展类加载器 --> 启动类加载器(最顶层)
  • 要求:除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
  • 工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,人因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父记载其反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
  • 琐碎知识点:
  • 被final修饰的字段会在编译期就把结果放入常量池中,直观体现就是引用该字段的,在编译出的class文件中就会被替换为该字段对应的数据。

程序编译与代码优化

编译过程
  • Java程序从源码编译成字节码和从字节码编译成本地机器码的过程,Javac字节码编译器与虚拟机内的JIT编译器的执行过程合并起来其实就等同于一个传统编译器所执行的编译过程。
Java语言是一门解释型语言吗?
  • Java语言严格意义上讲,既不是一门纯粹的解释型语言,也不是一门纯粹的编译型语言,它是一门两者都包含的语言。
HotSpot的解释器与编译器大概工作流程
  • 在部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的的运行特别频繁时,就会把这些代码认定为"热点代码"。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的比那极其称为即时编译器(Just In Time Compiler,下文中简称JIT编译器)。
解释器与编译器的优势比较
  • 解释器
  • 当程序需要迅速启动和执行的时候,解释器首先发挥作用,省去变异的时间,立即执行。
  • 编译器
  • 将"热点代码"编译成本地代码之后,可以获取更高的执行效率。
JDK1.7之后HotSpot采取的编译策略
  • 分层编译策略
  • 第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译。
  • 第1层,也称为C1(Client Compiler)编译,将字节码编译为本地代码,进行简单、可靠的优化,如由必要将加入性能监控的逻辑。
  • 第2层(或2层以上),也称为C2(Server Compiler)编译,也是将自己吗编译为本地代码,但是会启用雨鞋编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
编译对象与触发条件
  • 编译对象(“热点代码”)
  • 被多次调用的方法
  • 被多次执行的循环体所在的方法
  • 什么是热点探测?
  • 判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection)。
  • 热点探测的两种判定方式
  • 基于采样的热点探测
  • 基于计数器的热点探测(HotSpot默认热点探测方式)

高效并发

  • volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但像num++这种复合操作,volatile无法保证其原子性。
  • volatile为何无法保证num++这种复合操作的原子性??
  • num++可以拆分为两步:num+1,num=某个值。假设有两个线程:A,B,假设num初始值为0,当A进行完num+1后,开始走B线程,当B线程将num值加到2后才开始接着走A线程的赋值,那么这时就会造成num变小为了1。因此无法保证num++这种复合操作的原子性。