zoukankan      html  css  js  c++  java
  • jvm相关自我总结和 VisualVM工具的使用

    idea 二个工具: jclasslib Hexview
    jdk监控工具 VisualVM工具的使用: https://www.ibm.com/developerworks/cn/java/j-lo-visualvm/index.html

    对象的创建过程?:
    new--> 申请存储空间, 创建对象,此时对象处于半初始化状态
    invokespecial #1 ---> 调用实例初始化方法,私有方法,父类构造方法
    astore_1 ---> 将对象和栈中的局部变量建立联系
    return -----> main函数跳出

    DCL和volatile的问题, volatile不能少, 因为对象创建这个过程有可能出现指令重排, new--> invokespecial --> astore_1 这个过程 有可能会优化成 new --> astore_1 ---.invokespecial
    这样会导致第一次判断实例是否为null, 此时对象处于半初始化状态,不为null, 从而返回, 破坏了单例模式

    synchronized的底层原理:
    java对象加了一个 synchronized 关键字
    编译成字节码, 通过反编译可以看到 加了一个监视器 (monitorenter, monitorexit),
    在jvm执行的过程中进行 锁升级,偏向锁--> 自旋锁 --->重量级锁
    更底层的实现是: 汇编指令 lock comxchg

    对象在内存中的存储布局: markword(8字节) class pointer(类型指针4字节,用于判断对象属于哪个类的实例), instance data(实例数据), padding(对其补充)
    markword, class pointer 是对象头,对象头部markword的信息有:存储对象自身运行时数据, 锁信息, 对象分代年龄, hashcode,线程持有的锁, 偏向线程ID, 偏向时间戳

    一个对象new出来, 此时是没有什么锁的, 当被 synchronized修饰,且只有一个线程来访问对象时候,此时, 会将这个线程id 标记到这个对象头(这就是偏向锁),这个线程一看是自己的线程id,就开始使用这个对象,

    当有多个线程都来争抢这个对象时候,此时锁升级,升级为 自旋锁, 每个线程会在本线程内生成一个 lock record, 并都尝试将 这个lock record 添加到这个对象的markword中, 添加成功的线程开始使用这个对象, 添加失败的线程会在这里自旋(CAS) , 当这里自旋的线程变得很多的时候,会很消耗资源, jdk6之前是有个调优设置: 自旋的线程超过cpu核数一半,或者自旋次数超过10次, 就开始将自旋锁升级为 重量级锁, 现在的jdk8和之后版本中是有个 自适应机制,jvm自己来管理什么时候升级锁

    重量级锁: 这里牵扯到二个概念: 用户态,内核态, 操作系统需要二种CPU状态, 内核态: 运行操作系统程序,操作硬件, 用户态: 运行用户程序, jvm执行代码,偏行锁和自旋锁处于用户态时候, 当锁升级为内核态时候, 向内核申请重量级锁, 重量级锁将所有自旋的线程放到一个队列中,排队执行,, 避免cpu空转

    锁消除: 代码在JIT即时编译时,通过对上下文进行分析,去除掉没必要的加锁请求,比如一个方法中,new StringBuffer,之后不停的append, 由于这个StringBuffer对象只是在这个方法中使用,且append方法被synchronized修饰,如果不停的加锁,解锁,会消耗cpu资源, 所以jvm会将这里的锁消除

    锁粗化: 代码在JIT即使编译时, 通过上下文分析,将多个锁合并为同一把锁,这样避免频繁加锁,解锁,而是共用同一把大锁, 比如一个方法中,new一个 stringBuffer while(true)代码块中,对这个buffer,append, jIT会优化: 将append的锁粗化,在while上面加一把锁

    对象如何定位: 有二种方式: 句柄池, 直接指针(栈中引用直接指向堆中实例对象的内存地址)hotspot虚拟机使用的是直接指针
    句柄池: 栈中引用指向堆中的句柄池,句柄池中保存了实例对象的内存地址和对象类型数据指针, 再通过句柄池中的地址间接找到堆中实例对象

    对象如何分配: 此处有一个逃逸分析: 分析对象的作用域是否仅仅在某个方法中, 否则别的方法也引用这个对象,此时叫方法逃逸,如果某个对象是方法的返回值,别的线程会通过调用方法使用这个对象,此时叫线程逃逸,方法中的对象的作用域如果仅仅是在这个方法中, 直接将此对象分配到栈空间, 以便方法运行完毕,隧栈弹出而自动销毁 ,无需垃圾收集器收集
    栈上分配失败,会尝试TLAB分配,(TLAB是eden区一部分,堆为每一个线程都分配一个 TLAB空间,用来分配对象,避免多个线程争夺同一块堆空间,TLAB空间小,很容易就满了,)之后就会将新对象,是否可以直接进入老年代,可以的话,就直接分配到老年代, 如果不可以,就会在eden区进行分配

    new对象创建过程:
    1: 检查这个对象对应的类是否加载 链接 初始化, 没有的话,就在双亲委派机制下 去加载这个类,

    2: 为对象分配内存: 计算对象占用空间大小,在堆中划分一块内存给该对象, 划分内存这里有二种方式,如果堆内存空间不规则,虚拟机就维护一个列表,用来记录哪些内存是空闲的, 空闲列表方式. 另一种 指针碰撞,堆空间很规整, 使用过的内存在一边,未使用过的内存在一边,临界点是有一个指针, 这个指针移动对象空间大小即可, 到底使用哪种方法取决于所选择的垃圾收集器是否有压缩的能力决定

    3: 处理并发安全问题: 多个对象创建争夺同一块内存区域引发并发安全问题, 解决: 使用 CAS+失败重试,保证原子性, 另一个方法是: TLAB(Thread Local Allocation Buffer) ,jvm为每一个线程在堆上都创建一小块区域内存空间 ,该线程创建对象,分配内存就从各自的TLAB的内存区域开始分配, 如果TLAB对应的内存区域不够用了,此时才会使用 CAS+失败重试

    4: 初始化分配到的空间: 内存分配完成之后,对该对象对应的字段赋初始值, 字段类型对应的零值, 比如int: 0, long:0, String "", 这就是对象的半初始化

    5: 设置对象的对象头: 设置对象头: 这个对象是哪个类的实例,怎样找到这个类的元数据 ,该对象的hash码,该对象的gc分代年龄(4字节,最大是15),等信息

    6: 执行init方法进行初始化: 设置完对象头之后,从虚拟机角度看, 一个新对象已经产生了, 从java程序看 这个对象才刚开始创建,他的构造方法还没有执行, 所有的字段都是默认零值, 接下来执行init方法,

    对象的内存布局:
    对象头: 包含二部分: clazz pointer(类型指针, 虚拟机通过这个指针确定对象属于哪个类的实例) , mark world (运行时元数据), 有 哈希值, gc分代年龄, 锁状态标志, 线程持有的锁, 偏向锁ID 偏向时间戳, 如果该对象是数组还需要记录数组的长度

    实例数据, 这里才是对象的真正存储的有效信息,即我们在程序代码中定义的各种类型的字段内容,无论是父类继承信息,还是子类定义的字段信息

    对其填充: 任何对象的大小都必须是8字节的整数倍,不足就对其填充

    对象的访问定位: 使用句柄, 直接指针
    句柄: 堆中单独一块内存作为句柄池, 栈中的引用指向的是句柄池中 该对象对应的句柄地址, 句柄包含了该该对象对应实例数据, 类型数据信息

    直接指针: 栈中的引用直接指向堆中该对象的实例数据, 实例数据中有该对象对应的类型数据信息地址
    hotspot使用的是直接指针, 速度快,少了一次句柄池访问

  • 相关阅读:
    Lyft Level 5 Challenge 2018
    Codeforces Round #514 (Div. 2)题解
    Bubble Cup 11
    不如来搞一下CDQ分治吧!
    Codeforces Round #331 (Div. 2)
    写一下中国剩余定理的证明
    codeforces Round#332Div2 题解
    GCPC2017 题解
    2017 USP Try-outs 题解
    CodeForce 387D. George and Interesting Graph
  • 原文地址:https://www.cnblogs.com/lvcai/p/13794149.html
Copyright © 2011-2022 走看看