jvm初探

Table of Contents

jvm

逃逸分析

  1. 栈上分配
  2. 同步省略 对方法内部的无意义synchronized优化
  3. 标量替换 对仅在某类内部使用的静态类,对象,如果其完全由基本类型构成,直接替换为基本类型
public static class Data {
    int uid;
    int pid;
}
public void test(){
    Data data = new Data();
}
//等价于
public void test(){
    int uid = 0;
    int pid = 0;
}

对象创建

  1. 判断对象的类是否加载,链接,初始化 jvm执行new 指令,首先检查指令的参数能否在metaspace的常量池中定位到一个类的符号引用,并检查类是否被加载,解析和初始化.(即判断类的元信息是否存在),如果无,就在双亲委派模式下,使用当前类的加载器以 ClassLoader+包名+类名为key查找class文件,如果找不到,抛出classNotFoundException异常,找到就进行类加载
  2. 为对象分配内存 cas保证并发,或者TLAB提前解决,计算对象占用空间,在heap中划分内存,引用变量的话,4个byte
  • 如果内存规整,jvm采用指针碰撞(bump the pointer)分配内存,用一个指针表示使用过的内存和未使用的内存的分界线,分配对象内存,移动指针就行
  • 如果内存不规整,jvm维护空闲列表(Free List),记录那些内存块可用,分配一个足够的内存快划分给对象实例
  • 内存是否规整,也取决与gc算法
  1. 初始化类信息
  2. 设置对象头
  • hashcode,gc的信息,锁信息.etc,jvm实现

JIT

热点探测

hotspot采用基于计数器的热点探测,为每个方法建立2个不同类型的计数器,方法调用计数器(Invocation counter),server模式是10000,和回边计数器(back edge counter),后者统计循环体执行的循环次数 通过 -XX:CompoileThresold 流程:

  1. 方法调用前先检查是否被JIT编译过
  2. 如果没有,方法调用计数器+1,检查是否大于阈值,大于提交JIT编译请求,然后解释器执行
  3. 如果存在,执行编译后的机器码

热度衰减

方法调用计数器统计的次数不是绝对次数,是相对执行频率,超过一定时间达不到调用次数,计数器衰减一半,这段时间称为半衰周期(count half life time) 通过 -XX:UseCounterDecay关闭热度衰减

配置

模式

  1. -Xint 纯解释器
  2. -Xcomp 纯JIT
  3. -Xmied 混合 -最好
  4. -client c1
  5. -server c2

GC

标记

  1. 引用计数 为每个对象保存一个整形的引用计数属性,无法处理循环引用
  2. 可达性分析 以根对象GC roots为起始点,按照从上至下的方式搜索gcroots连接的目标对象是否可达,执行后,搜索走过的路径称为引用链reference chain,目标对象无任何Rc,视为不可达 Gc roots对象
  3. jvm的栈引用对象
  4. 本地方法栈内JNI的引用对象
  5. meta space的类静态属性引用对象
  6. meta space的常量引用对象
  7. 所有被synchronized持有对象
  8. jvm内部引用
  9. JMXBean,JVMTI中注册的回调,本地代码缓存.etc jdk8之前,Object存在finalize方法,也就是类销毁之前的操作,这给了一个类的复活可能,但是jdk9之后已经@Deprected了,可以想象到的是不正确的操作会导致gc严重的性能问题

清除

  1. 标记清除 注意此时和标记阶段无关,此算法依然要执行标记,标记阶段是标记可达对象,清除阶段是对堆内存从头到尾线性遍历,如果发现某个对象未被标记为可达,回收,将对象地址保存在空闲列表内,效率不高,会产生大量内存碎片
  2. 复制 2倍内存,将所有存活对象复制到新region,避免了内存碎片,无标记清除过程,Gc需要维护对象的引用关系,原因是java的对象引用是直接引用,并且此过程是复制不是移动,此算法主要用于存活对象较少,垃圾对象较多时,edn s1 s2
  3. 标记压缩 一阶段和标记清除相同,二阶段将存活对象压缩到内存一端,需要STW