JVM学习随笔

JVM规范:
    Class文件格式
    数字的内部表示和存储
        -Byte -128 to 127
    returnAddress数据类型定义:
        -指向操作码的指针。不对应java数据类型,不能再运行时修改。
    定义PC
    堆
    栈
    方法区

JVM运行机制:

    JVM启动流程:
        如图:
    JVM基本结构:
        如图:
        PC寄存器:
            - 每个线程拥有一个PC寄存器
            - 在线程创建时 创建
            - 指向吓一跳指令的地址
            - 执行本地方法时,PC的值为undefined
        方法区:
            - 保存类的源信息
                -类的常量池
                -字段、方法的信息
                -方法字节码
            - 通常和永久去(Perm)关联再一起
        Java堆:
            - 和程序开发密切相关
            - 应用系统对象都保存在java堆中
            - 所有的线程共享java堆
            - 对分代GC来说,堆也是分代的
            - GC的主要工作空间
                -----------------------------------------
                |        |        |        |                |
                |  eden    |    s0    |    s1    |    tenured        
                |        |        |        |                |
                -----------------------------------------
                                复制算法
        java栈:
            - 线程私有的
            - 栈由一系列的帧组成(因此java栈也叫作帧栈)
            - 栈保存一个方法的局部变量、操作数栈、常量池指针
            - 每一次方法调用创建一个帧,并压栈

        
    内存模型:
        - 每一个线程有一个工作内存和主存独立
        
        - 工作内存存放主存中变量值的拷贝
                ----------------
                | 线程执行引擎    |
                ----------------
                    |        ^
            assign  |        |    use
                    V        |
                ----------------
                | 线程工作内存    |
                ----------------    
                    ^        |
        read,load   |        |    store,write
                    |        V
                ----------------
                |    主内存        |
                ----------------    
            注:当数据从主存复制到工作内存是,必须出现两个动作:
                    1:与主内存执行读(read)操作
                    2:由工作内存执行相应的load操作
                当数据从工作内存拷贝到主内存时,也出现两个操作:
                    1:由工作内存执行的存储(store)操作
                    2:由主内存执行相应的写(write)操作
                每一个操作都是原子的,即执行期间不会被中断
                对于普通变量,一个线程中更新的值,不能马上反应再其他的变量中
                如果需要在其它线程中立即可见,需要是用volatile关键字
        
        可见性:
            - 一个线程修改了变量,其他线程可以立即知道
        
            - 保证线程可见性的方法:
                - volatile
                - synchronized(unlock之前,写变量值会主存)
                - final(一旦初始化完成,其他线程就可见)
        有序性:
            - 在本线程内,操作都是有序的
            - 在线程外观察,操作都是无序的
    
JVM常用的配置参数:

    Trace跟踪参数:
        *:-verbose:gc
        *:-XX:+pringGC
        *:可以打印GC的简要信息
            ---[GC 4790K->364K(15782K),0.001606 secs]
            ---[GC 4790K->364K(15782K),0.001474 secs]
            ---[GC 4790K->364K(15782K),0.001563 secs]
            ---[GC 4790K->364K(15782K),0.001682 secs]
        *-XX:+PrintGCDetails
            -打印GC详细信息
        *-XX:+PrintGCTimeStamps
            -打印GC发生的时间戳
        *-Xloggc:log/gc.log
            -指定GC log的位置,以文件输出
            - 帮助开发人员分析问题
        *-XX:+PringHeapAtGC
            -每一次GC前后,都打印堆信息
        *-XX:+TraceClassLoading
            -监控类的加载
        *-XX:+PrintClassHistogram
            -按下Ctrl + Break以后,打印类的信息
            分别显示:序号、实例数量、总大小、类型 (这个参数可以看各个数据类型的使用情况)
    
    堆的分配参数:
        -Xmx -Xms
            -指定最大堆和最小堆
            -Xmx20m -Xms5m
        -Xmn
            -设置新生代的大小
        -XX:NewRatio
            -新生代(eden + 2*s)和老年代(不包含永久区)的比值
            -4表示 新生代:老年代 = 1:4,即年轻代占堆的1/5
        -XX:SurvivorRatio
            -设置两个Survivor区和eden的比
            -8表示 两个Survivor:eden = 2:8 一个Survivor也就是占年轻代1/10
        -XX:+HeapDumpOnOutOfMemoryError    
            -OOM时导出到堆
        -XX:+HeapDumpPath
            -导出OOM的路劲
        -XX:OnOutOfMemoryError
            -在oom时,执行一个脚本
            -"-XX:OnOutOfMemoryError=xxxx.bat %P"  %P java进程ID 当程序oom时就会执行bat文件  可以用来发邮件甚至重启服务
        
            堆的分配参数总结:
                根据实际情况调整新生代和心存代的大小
                官方推荐新生代占堆的3/8
                辛存代占新生代的1/10
                在oom时,记得Dump出堆,确保可以排查现场问题
                
        永久区分配参数:
            -XX:PermSize -XX:MaxPermSize
                -设置永久带的初始空间和最大空间
                -他们表示,一个系统可以容纳多少个类型
    
    栈大小分配:
        -Xss
            -通常只有几百K
            -决定了函数调用的深度
            -每个线程都有独立的栈空间
            -局部变量、参数 分配在栈上
        
GC算法与种类:

    GC的概念:垃圾收集,在java中,GC的对象是堆空间和永久区。
    
    GC算法:
        引用计数法:
            引用计数法的概念
                对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,
                则A对象就不可能再被使用。
            引用计数法的问题:
                -引用计数法伴随着加法和减法,影响性能。
                -很难处理循环引用。
        
        标记-清除算法:
            概念:
                标记-清除算法分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。
                因此,从未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象。
        
        标记-压缩算法:
            概念:
                标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记清除算法一样。标记压缩也需要从根节点开始。
                对所有可达对象做一次标记。但之后,它并不简单的清除未标记对象,而是将所有的存活对象压缩到内存的一端之后,清理边界外所有空间。
        
        复制算法:
            -与标记-清除算法相比,复制算法是一种相对高效的回收方法
            -不适用于存活对象比较多的场合
            -将原有内存空间分为两块,每次只使用其中一块,在垃圾收回时,将正在使用的内存中存活的对象复制到未使用的内存块中,之后,清除真的使用的内存块
                中的所有对象,交换两个内存的角色,完成垃圾回收。
            复制算法的问题:
                空间浪费,需预留一半的空间。
            
    分代思想:
        -根据对象的存活周期进行分代,短命对象归为新生代,长命对象归为老年代。
        -根据不同代的特点,选取合适的收集算法:
            -少量对象存活适合复制算法
            -大量对象存活适合标记清理或者标记压缩
            
    GC算法总结:所有的算法,需要一个识别的垃圾对象,因此需要给出一个可触及性的定义。
        引用计数
            -没有被java采用
        标记-清除
        标记-压缩
        复制算法
            -新生代
            
    可触及性:(什么是根:栈中的对象,全局对象,JNI方法栈中引用的对象)
        可触及:
            -从根节点可以触及到这个对象
        可复活:
            -一旦所有引用被释放,就可复活的状态
            -因为在finallize()中可能复活该对象
        不可触及的:
            -在finallze()后,可能进入不可触及的状态
            -不可触及的对象不可能复活
            -可以回收
            
    Stop-The-World
        是什么:
            -java中一种全局暂停的现象
            -全局停顿,所有java代码停止,native代码可以执行,但不能和jvm交互。
            -多半由于GC引起
                -Dump线程
                -死锁检查
                -堆Dump
                
        为什么:
            -停止制造垃圾才能打扫干净。
        
        危害:
            -长时间服务停止,没有响应。
            -遇到HA系统,可能引起主备切换,严重危害生产环境。
            
GC回收器:
    GC串行回收器:
        -最古老,最稳定
        -效率高
        -可能会产生较长的停顿
        -XX:+UseSerialGC
            -新生代、老年代使用串行回收
            -新生代复制算法
            -老年代标记-压缩算法
            
    GC并行回收器:
        ParNew收集器:
            -XX:+UseOarNewGC
                -新生代并行
                -老年代串行
            -Serial收集器新生代的并行版本
            -复制算法
            -多线程,需要多核支持
            - -XX:ParallelGCThreads 限制线程数量
        
        Parallel收集器:
            -类似ParNew
            -新生代复制算法
            -老年代 标记-压缩
            -更加关注吞吐量
            - -XX:+UseParallelGC
                -使用parallel收集器+老年代串行
            - -XX:+UserParallelOldGC
                -使用Parallel收集器+并行老年代

        并行回收器的参数:
            -XX:MaxGCPauseMills
                -最大停顿时间,单位毫秒
                -GC尽力保证回收时间不超过设定值
            -XX:GCTimeRatio
                -0-100的取值范围
                -垃圾收集时间占总时间的比
                -默认99,即最大允许1%时间做GC
            这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优

    CMS收集器:
        -Concurrent Mark Sweep 并发标记清除
        -与标记-清除算法
        -与标记-压缩相比
        -并发阶段会降低吞吐量
        -老年代收集器(新生代使用ParNew)
        - -XX:+UseConcMarkSweepGC
        
        CMS的运行过程:
            -初始标记
                -根可以直接关联到的对象
                -速度快
            -并发标记(和用户线程一起)
                -主要标记过程,标记全部对象
            -重新标记
                由于并发标记时,用户线程任然运行,因此在正式清理前,再做修正
            -并发清除(和用户线程一起)
                基于标记结果,直接清理对象
        
        CMS的特点:
            -尽可能的降低停顿
            -会影响系统整体吞吐量和性能
                -比如在用户线程运行过程中,分一半CPU去做GC,反应速度就下降一半
            -清理不彻底
                -因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
            -因为和用户线程一起运行,不能在空间快满时再清理
                - -XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
                -如果不幸内存预留空间不够,就会引起concurrent failure

        GC参数 – CMS收集器
            -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理
                整理过程是独占的,会引起停顿时间变长
            -XX:+CMSFullGCsBeforeCompaction 
                设置进行几次Full GC后,进行一次碎片整理
            -XX:ParallelCMSThreads
                设定CMS的线程数量

    GC参数整理:
        -XX:+UseSerialGC:在新生代和老年代使用串行收集器
        -XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
        -XX:NewRatio:新生代和老年代的比
        -XX:+UseParNewGC:在新生代使用并行收集器
        -XX:+UseParallelGC :新生代使用并行回收收集器
        -XX:+UseParallelOldGC:老年代使用并行回收收集器
        -XX:ParallelGCThreads:设置用于垃圾回收的线程数
        -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
        -XX:ParallelCMSThreads:设定CMS的线程数量
        -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
        -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
        -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
        -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
        -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
        -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

    GC参数-Tomcat实例:
        环境:
            Tomcat7
            jsp网站
            测试网站吞吐和延时
        工具:
            JMeter
        目的:
            让tomcat有一个不错的吞吐量

类装载器:
    class装载验证流程:
        加载:
            装载类的第一个阶段
            1:取得累的二进制流
            2:转为方法区数据结构
            3:在java堆中生成对应的java.lang.Class对象
            
        链接--> 验证:
            目的:保证Class流的格式是正确的
                -文件格式的验证
                    -是否以0xCAFEBABE开头
                    -版本号是否合理
                -元数据验证
                    -是否有父类
                    -继承了final类?
                    -非抽象类实现了所有的抽象方法
                -字节码验证 (很复杂)
                    -运行检查
                    -栈数据类型和操作码数据参数吻合
                    -跳转指令指定到合理的位置
                -符号引用验证
                    -常量池中描述类是否存在
                    -访问的方法或字段是否存在且有足够的权限
        
        链接--> 准备:
            -分配内存,并为类设置初始值 (方法区中)
                public static int v=1;
                在准备阶段中,v会被设置为0
                在初始化的<clinit>中才会被设置为1
                对于static final类型,在准备阶段就会被赋上正确的值
                public static final  int v=1;

        链接--> 解析:
            -符号引用替换为直接引用
            
        初始化:
            执行类构造器<clinit>
                static变量 赋值语句
                static{}语句
            子类的<clinit>调用前保证父类的<clinit>被调用
            <clinit>是线程安全的

什么是类装载器ClassLoader:
    ClassLoader是一个抽象类
    ClassLoader的实例将读入Java字节码将类装载到JVM中
    ClassLoader可以定制,满足不同的字节码流获取方式
    ClassLoader负责类装载过程中的加载阶段


系统性能监控
    性能监控 - linux:
        uptime:
            系统时间
            运行时间
            连接数
            1,5,15分钟内的系统平均负载
        top:
            同uptime
            CPU
            内存
            每个进程占CPU的情况
        vmstat:
            可以统计系统的CPU,内存,swap,io等情况
            CPU占用率很高,上下文切换频繁,说明系统有线程正在频繁切换
        pidstat:
            细致观察进程
            需要安装
                sudo apt-get install sysstat
            监控CPU
            监控IO
            监控内存
            ps:pidstat -p 2962 -u 1 3 -t (2962/进程号 -u/监控CUP 每秒一次 一共三次 -t/显示线程)
        
    性能监控 - windows:
        pslist
            -命令行工具
            -可用于自动化数据收集
            -显示java程序的运行情况

    Java自带的工具:
        jps:
            -列出java进程,类似于ps命令
            -参数-q可以指定jps只输出进程ID ,不输出类的短名称
            -参数-m可以用于输出传递给Java进程(主函数)的参数
            -参数-l可以用于输出主函数的完整路径
            -参数-v可以显示传递给JVM的参数
            
        jinfo:
            可以用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数
            -flag <name>:打印指定JVM的参数值
            -flag [+|-]<name>:设置指定JVM参数的布尔值
            -flag <name>=<value>:设置指定JVM参数的值
            
        jmap:
            -生成Java应用程序的堆快照和对象的统计信息
            -jmap -histo 2972 >c:\s.txt
            
        Dump堆:
            jmap -dump:format=b,file=c:\heap.hprof 2972
        
        jstack:
            打印线程dump
            -l 打印锁信息
            -m 打印java和native的帧信息
            -F 强制dump,当jstack没有响应时使用

        JConsole:
            图形化监控工具
            可以查看Java应用程序的运行概况,监控堆信息、永久区使用情况、类加载情况等
    
        - Visual VM:
            Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具
    

内存溢出(OOM)的原因:
    堆溢出:
        占用大量堆空间,直接溢出,Exception in thread "main" java.lang.OutOfMemoryError: 
    永久区:
        生成大量的类,无法回收,Caused by: java.lang.OutOfMemoryError: PermGen space 解决方法:增大Perm区 允许Class回收
    Java栈溢出:
        这里的栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,
        如果操作系统无法给出足够的空间,就会抛出OOM。解决方法:减少堆内存 减少线程栈大小
    直接内存溢出:
        ByteBuffer.allocateDirect()无法从操作系统获得足够的空间,解决方法:减少堆内存 有意触发GC

锁:
    对象头Mark:
        -Mark Word,对象头的标记,32位
        -描述对象的hash、锁信息,垃圾回收标记,年龄
            -指向锁记录的指针
            -指向monitor的指针
            -GC标记
            -偏向锁线程ID
    
    偏向锁:
        大部分情况是没有竞争的,所以可以通过偏向来提高性能
        所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
        将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
        只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
        当其他线程请求相同的锁时,偏向模式结束
        -XX:+UseBiasedLocking
            -默认启用
        在竞争激烈的场合,偏向锁会增加系统负担
    
    轻量级锁:BasicObjectLock    
        -普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
        -如果对象没有被锁定
            -将对象头的Mark指针保存到锁对象中
            -将对象头设置为指向锁的指针(在线程栈空间中)
        -如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
        -在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
        -在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降
    
    自旋锁:
        -当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)
        -JDK1.6中-XX:+UseSpinning开启
        -JDK1.7中,去掉此参数,改为内置实现
        -如果同步块很长,自旋失败,会降低系统性能
        -如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

    偏向锁,轻量级锁,自旋锁总结:
        -不是Java语言层面的锁优化方法
        -内置于JVM中的获取锁的优化方法和获取锁的步骤
            -偏向锁可用会先尝试偏向锁
            -轻量级锁可用会先尝试轻量级锁
            -以上都失败,尝试自旋锁
            -再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

    基于java代码层面锁的优化:
        减少锁持有时间,也就是没必要做同步的方法就尽量不要去做同步,能在方法上别在类上
        减小锁粒度:
            -将大对象,拆成小对象,大大增加并行度,降低锁竞争
            -偏向锁,轻量级锁成功率提高
            -ConcurrentHashMap
            -HashMap的同步实现
                -Collections.synchronizedMap(Map<K,V> m)
                -返回SynchronizedMap对象
        锁分离:
            根据功能进行锁分离
            ReadWriteLock
            读多写少的情况,可以提高性能
            
        锁粗化:
            通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。
            只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,
            其本身也会消耗系统宝贵的资源,反而不利于性能的优化
        
        锁消除:
            在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作
    
        无锁:
            -锁是悲观的操作
            -无锁是乐观的操作
            -无锁的一种实现方式
                -CAS(Compare And Swap)
                -非阻塞的同步
                -CAS(V,E,N)
                    CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,
                    才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
                    CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,
                    并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,
                    CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
            -在应用层面判断多线程的干扰,如果有干扰,则通知线程重试














            
            

 

posted @ 2018-09-09 15:23  鲸落-k  阅读(165)  评论(0编辑  收藏  举报