zoukankan      html  css  js  c++  java
  • JVM性能优化简介

    01. JVM是什么
        概述:
            大白话:
                全称Java Virtual Machine(Java虚拟机), 它是一个虚构出来的计算机, 通过实际的计算机来模拟各种计算机的功能.
            专业版:
                JVM是一个进程, 用来模拟计算单元, 将.class字节码文件转成计算机能够识别的指令.
            //这里可以联想以前大家学的"VM ware", 它也是一个虚拟机.
            //它们的区别就在于: VM Ware是你能看见的, JVM是你看不见的.
            
        回顾:
            我们以前写的Java程序是: 编写 --> 编译 --> 运行三个阶段的.
            .class文件是Java语言独有的, 只有JVM能识别, 其他任何软件都识别不了.
            所以Java语言的"跨平台性(一次编译到处运行)"就是由JVM来保证的.
            
        画图演示:
            JVM把.class字节码文件 转成 计算机能够识别的指令的过程.
            
        代码演示:
            D:compileWorker.java文件, 通过"jps"命令查看启动的进程.
            
        
    02. JVM虚拟机运行的流程
        JVM是一个进程,接下来我们来研究它的: 工作机制, 这个问题是很深奥的, 不亚于研究一个完整VM Ware虚拟机,但是诸如"硬盘, CD/DVD这些部分和我们都没关系", 所以研究JVM的工作机制就是在研究它的: 运算机制.
        首先, 请你思考一个问题: 如果我给你一个A.class字节码文件, 想把它运行起来, 你会做哪些事情?
        
        画图演示:
            1. 读取字节码文件所在的路径.
                //类加载机制
            2. 获取字节码文件中具体的内容.
                //方法区: 用来存放类的描述信息.
            3. 获取该类的实例(对象)
                //堆(Heap): 用来存储对象的(所有new出来的内容)
            4. 通过对象名.的方式调用方法.
                //栈(Stack): 用来存放局部变量及所有代码执行的.
            
        今天我们的学习顺序, 就是按照这个流程来走的.


    03. JVM虚拟机类加载机制(一):运行顺序
        首先, 我们先来研究JVM的类加载机制, 类加载机制就是把类给读取出来, 我们来看一下它是如何运行的.
        画图演示:
            JVM底层加载类依靠三大组件:
                BootStrapClassLoader    //启动类加载器
                    //负责加载: jrelib t.jar        //rt: runtime, 运行的意思
                    //windows最早不支持java, 没有JRE, 后来Sun公司打官司赢了, windows开始默认支持JRE.
                ExtClassLoader:            //扩展类加载器
                    //负责加载: jrelibext* 文件夹下所有的jar包
                    //这两个加载器执行完毕后, JVM虚拟机基本上就初始化完毕了.
                APPClassLoader:            //应用程序类加载器
                    //负责加载: 用户自定义的类的.
                    //就是加载: 用户配置的classpath环境变量值的.
                //UserClassLoader        //自定义类加载器
                    //自定义类加载器就是自定义一个类继承ClassLoader, 然后重写findClass(), loadClass()两个方法即可.
                    
            加载顺序是:     BootStrap --> ExtClassLoader --> AppClassLoader --> UserClassLoader
            
        代码演示:
            1) 随便编写一个A类, 然后演示: jar包的加载过程(rt.jar, ext*等相关的jar包)
            2) 打印类加载器对象:
                 //1. 获取当前线程的类加载器
                ClassLoader load = Thread.currentThread().getContextClassLoader();
                //2. 打印当前线程的类加载器.
                System.out.println(load);                           //AppClassLoader
                //3. 打印当前线程的类加载器的父类(加载器).
                System.out.println(load.getParent());               //ExtClassLoader
                //4. 打印当前线程的类加载器的父类的父类(加载器).
                System.out.println(load.getParent().getParent());   //null: 其实应该是BootStrapClassLoader, 但是它是C语言写的, 所以打印不出来.
            
            
    04) JVM虚拟机类加载机制(二):检查顺序
        刚才我们学完了JVM类加载机制的"加载循序", 现在, 我们来研究下它的"检查顺序", 请你思考,
        假设: D:compile, ext*.jar, rt.jar三类中都有 A.class, 那么A.class是否会被加载3次, 如果不会, 它的加载顺序是什么样的?
            不会, BootStrap会加载A.class.
            
        运行顺序是:
            bootstrap --> ext --> app
            1) bootstrap先加载 A.class
            2) ext检查A.class是否加载:
                是: 不加载A.class
                否: 加载A.class
            3) app检查A.class是否加载:
                是: 不加载A.class
                否: 加载A.class
            
        例如:
            UserClassLoader
            APPClassLoader
            ExtClassLoader
            BootStrapClassLoader
        总结:
            自上而下检查, 自下而上运行.
        
        
    05) JVM的内存模型(方法区, 堆区, 栈区, 程序计数器)
        到目前为止我们已经知道类加载器是用来加载字节码文件的, 那加载完字节码文件之后, 是不是要运行起来啊?
        那它是怎么运行的呢? 在我的课件中有一个"JVM运行时内存数据区", 接下来我们详细的来学习一下.
        
        1) A.class字节码文件被加载到内存.
            //存储在方法区中, 并且方法区中也包含常量池.
            
        2) 创建本类的实例对象, 存储在堆中(heap)
        
        3) 通过对象名.的形式调用方法, 方法执行过程是在: 虚拟机栈中完成的.
            //一个线程对应一个虚拟机栈, 每一个方法对应一个: 虚拟机栈中的栈帧
        
        4) 程序计数器区域记录的是当前程序的执行位置, 例如:
            线程1: print(), 第3行
        
        5) 将具体要执行的代码交给: 执行引擎来执行.
        
        6) 执行引擎调用: 本地库接口, 本地方法库来执行具体的内容.    
            //这部分了解即可, 用native修饰的方法都是本地方法.
            
        7) 本地方法栈: 顾名思义, 就是本地方法执行的区域.(C语言, 外部库运行的空间)
            //了解即可.
        
        8) 直接内存: 大白话翻译, 当JVM内存不够用的时候, 会找操作系统"借点"内存.
            //了解即可.
        
    06) JVM的一个小例子    
        1) 编写源代码.
            //创建一个A类, 里边有个print()方法.
            public class A {
                public void print() {
                    System.out.println("h");
                    System.out.println("e");
                    System.out.println("l");
                    System.out.println("l");    
                    System.out.println("o");
                }
            }
        
        2) 在A类中, 编写main()函数, 创建两个线程, 分别调用A#print()方法.
            /*
                java A  //运行Java程序
                加载类:
                    1) bootstrap 加载rt.jar
                    2) ext 加载 jrelibext*.jar
                    3) app 加载 A.class
                具体运行:
                    1) 主函数运行. 栈中有个主线程, 调用MainThread.main();
                    2) 执行第23行,  A a = new A(); 将a对象存储到堆区.
                    3) 执行第24行, 调用a.print()方法, 生成一个栈帧, 压入主线程栈.
                    -----> 执行, 运行print()方法的5行代码.

                    4) 栈中有个新的线程, t1,
                        t1 --> run栈帧 --> print栈帧
                    5) 栈中有个新的线程, t2,
                        t2 --> run栈帧 --> print栈帧

             */
            public class A {
                public void print() {
                    System.out.println("h");
                    System.out.println("e");
                    System.out.println("l");
                    System.out.println("l");
                    System.out.println("o");
                }

                public static void main(String[] args) {
                    A a = new A();
                    a.print();

                    //创建两个线程对象, 调用A#print();
                    //线程是CPU运行的基本单位, 创建销毁由操作系统执行.
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            a.print();
                        }
                    }).start();

                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            a.print();
                        }
                    }).start();
                }
            }

        3) 画图演示此代码的执行流程.
        4) 时间够的情况下, 演示下: 守护线程和非守护线程.
            
            
            
    07) 线程安全和内存溢出的问题
        到目前为止, 大家已经知道了JVM的内存模型, 也知道了各个模块的作用,
        接下来, 请你思考一个问题: 上述的模块中, 哪些模块会出现线程安全的问题,
        哪些模块有内存溢出的问题?
        
        举例:
            public class A{
                int i;
                
                public void add() {
                    i++;
                }
            }
            //当两个线程同时调用add()方法修改变量i的值时, 就会引发线程安全问题.
        画图演示上述代码.
        
        结论:
            1) 存在线程安全问题的模块.
                堆: 会.         //多线程, 并发, 操作同一数据.
                栈:    不会.        //线程栈之间是相互独立的.
                方法区:    不会.    //存储常量, 类的描述信息(.class字节码文件).
                程序计数器:不会.//记录程序的执行流程.
            
            2) 存在内存溢出问题的模块.
                堆: 会.         //不断创建对象, 内存被撑爆.
                栈: 会.         //不断调用方法, 内存被撑爆.
                方法区: 会.     //常量过多, jar包过大, 内存被撑爆.
                程序计数器: 会. //理论上来讲会, 因为线程过多, 导致计数器过多, 内存被撑爆.
            
        其实我们研究JVM性能优化, 研究的就是这两个问题, 这两个问题也是常见面试题.
        //面试题:说一下你对 线程安全和内存溢出这两个问题的看法.
        
        总结:
            研究这两个问题, 其实主要研究的还是"堆(Heap)内存".
            
            
    08) JDK1.7的堆内存的垃圾回收算法
        JDK1.7 将堆内存划分为3部分: 年轻代, 年老代, 持久代(就是方法区).
        年轻代又分为三个区域:    //使用的是 复制算法(需要有足够多的空闲空间).
            Eden: 伊甸园
                //存储的新生对象, 当伊甸园满的时候, 会将存活对象复制到S1区.
                //并移除那些垃圾对象(空指针对象).
            Survivor: 幸存者区1
                //当该区域满的时候, 会将存活对象复制到S2区
                //并移除那些垃圾对象.
            Survivor: 幸存者区2
                //当该区域满的时候, 会将存活对象复制到S1区.
                //并移除那些垃圾对象.
            大白话翻译:
                s1区 和 s2区是来回互相复制的.
        
        年老代:    //使用的是标记清除算法, 标记整理算法.
            //当对象在S1区和S2区之间来回复制15次, 才会被加载到: 年老代.
            //当年轻代和年老代全部装满的时候, 就会报: 堆内存溢出.
        
        持久代:    //就是方法区
            存储常量, 类的描述信息(也叫: 元数据).
        
            
    09) JDK1.7默认垃圾回收器    //所谓的回收器, 就是已经存在的产品, 可以直接使用.
        Serial收集器:
            单线程收集器, 它使用一个CPU或者一个线程来回收对象,
            它在垃圾收集的时候, 必须暂停其他工作线程, 直到垃圾回收完毕.
            //类似于: 国家领导人出行(封路), 排队点餐(遇到插队现象)
            //假设它在回收垃圾的时候用了3秒, 其他线程就要等3秒, 这样做效率很低.
            
        ParNew收集器:
            多线程收集器, 相当于:  Serial的多线程版本.
            
        
        Parallel Scavenge收集器:
            是一个新生代的收集器,并且使用复制算法,而且是一个并行的多线程收集器.
            其他收集器是尽量缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量:
                吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
                (虚拟机总共运行100分钟,垃圾收集时间为1分钟,那么吞吐量就是99%)
            //因为虚拟机会根据系统运行情况进行自适应调节, 所以不需要我们设置.
            
        CMS收集器:    //主要针对于年老代.
            整个过程分为:
                初始标记;    //用户线程等待
                并发标记;    //用户线程可以执行
                重新标记;    //用户线程等待
                并发清除;    //用户线程可以执行
            可以理解为是:
                精细化运营, 前边的垃圾收集器都是一刀切(在回收垃圾的时候, 其他线程等待), 而CMS是尽可能的降低等待时间, 并行执行程序, 提高运行效率.
        以上为JDK1.7及其以前的垃圾回收器, JDK1.8的时候多了一个: G1.
        G1在JDK1.9的时候, 成为了默认的垃圾回收器.
        
        
        
    10) VM宏观结构梳理
        1) Java程序的三个阶段:
            编写: A.java
            编译: javac A.java
            运行: java A.class
            
        2) 类加载器
            bootstrap
            ext
            app
            
        3) JVM的内存结构
            堆:
                年轻代
                年老代
                持久代(也就是方法区)
                    元数据(类的描述信息, 也就是.class字节码文件), 常量池
                
            栈:
                有n个线程栈, 每个线程栈又会有n个栈帧(一个栈帧就是一个方法)
                
            程序计数器:
                用来记录程序的执行流程的.
                
            本地方法栈:
                C语言, 外部程序运行空间.
                
    11) G1垃圾回收器
        在上个图解上做优化, 用G1新图解, 覆盖之前堆中的内容.
        
        1) 将内存划分为同样大小的region(区域).
        2) 每个region既可以是年轻代, 也可以是老年代, 还可以是幸存者区.
        3) 程序运行前期, 创建大量对象的时候, 可以将每个region看做是: Eden(伊甸园).
        4) 程序运行中期, 可以将eden的region变成old的region.
        5) 程序运行后期, 可以缩短Eden, Survivor的区域, 变成Old区域.
            //这样做的好处是: 尽可能大的利用堆内存空间.
        6) H: 存储大对象的.
        7) G1是JDK1.8出来的, 在JDK1.9的时候变成了: 默认垃圾处理器.
        
        
    12) G1中的持久代(方法区)不见了
        方法区从JVM模型中迁移出去了, 完全使用系统的内存.
        方法区也改名叫: 元数据区.
        
        
    13) 内存溢出的代码演示
        1) 堆内存溢出演示: main.java.heap.PrintGC_demo.java
            //创建对象多, 导致内存溢出.
            
        2) 栈内存溢出演示:
            main.java.stack.StackOverFlow(递归导致的)
            //不设置的话在5000次左右, 设置256K后在1100次左右.
            
            main.java.stack.Thread(不断创建线程导致的)
            //这个自行演示即可, 电脑太卡, 影响上课效果.
            
        3) 方法区内存溢出演示:
            main.java.method.MethodOOM        //常量过多
            main.java.direct.DirectMenOOM    //jar包过大, 直接溢出.
            
        总结:
            可能你未来的10年都碰不到JVM性能调优这个事儿, 先不说能不能调优, 而是大多数的
            公司上来就撸代码, 很少会有"JVM调优"这个动作, 即使遇到了"JVM调优", 公司里边
            还有架构师呢, 但是我们马上要找工作了, 把这些相关的题了解了解, 看看, 对面试会
            比较有帮助.
            //JVM调优一般是只看, 不用, 目前只是为了面试做准备.
            
    14) 引用地址值比较
        直接演示src.main.method.ATest类中的代码即可.
        //讲解==比较引用类型的场景.
            
        
    15) JVM调优案例赏析
        百度搜索 --> JVM调优实践, 一搜一大堆的案例.
        
        
    16) GC的调优工具jstat        //主要针对于GC的.
        1) 通过Dos命令运行 D:compileWorker.java
            
        2) 重新开启一个Dos窗口:
            //可以通过jps指令查看pid值.
            jstat -class 2041(Java程序的PID值)        //查看加载了多少个类
            jstat -compiler 2041(Java程序的PID值)    //查看编译的情况
            jstat -gc 2041(Java程序的PID值)            //查看垃圾回收的统计        
            jstat -gc 2041 1000 5                    //1秒打印1次, 总共打印5次
            
    17) GC的调优工具jmap        //主要针对于内存使用情况的.
        1) 通过Dos命令运行 D:compileWorker.java
        
        2) jmap -heap 2041(Java程序的PID值)            //查看内存使用情况
           jmap -histo 2041 | more                    //查看内存中对象数量及大小
           jmap -dump:format=b,file=d:/compile/dump.dat 2041    //将内存使用情况dump到文件中
           jhat -port 9999 d:/compile/dump.dat                  //通过jhat对dump文件进行分析
                //端口号可以自定义, 然后在浏览器中通过127.0.0.1:9999就可以访问了.
                
                
    18) GC的调优工具jstack-死锁      //针对于线程的.  
        1) 线程的六种状态:
            新建, 就绪, 运行(运行的时候会发生等待或者阻塞), 死亡.
            
        2) 编写一个死锁的代码.
            //两个线程, 两把锁, 一个先拿锁1, 再拿锁2, 另一个先拿锁2, 在拿锁1.
            
        3) 通过jstack命令可以查看Java程序状态.  
            jstack 2041        //查看死锁状态
            
                                                    
    19) GC的可视化调优工具    //jstat, jmap, jstack
        1) 本地调优.
            1.1) 该工具位于 JDK安装目录/bin/jvisualvm.exe
                //双击可以直接使用.
                
            1.2) 以IntelliJ Platform为例, 演示下各个模块的作用.
                
            1.3) 该工具涵盖了上述所有的命令.
            
        2) 远程调优.        //自行测试(目前先了解即可).
            java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=9999 DeadLock
            
            这几个参数的意思是:
                -Dcom.sun.management.jmxremote :允许使用JMX远程管理
                -Dcom.sun.management.jmxremote.port=9999 :JMX远程连接端口
                -Dcom.sun.management.jmxremote.authenticate=false :不进行身份认证,任何用户都可以连接
                -Dcom.sun.management.jmxremote.ssl=false :不使用ssl
        
        
    20) JVM的总结
        1) 什么是JVM?
        2) JVM类加载机制.
            //bootstrap, ext, app
        3) JVM内存模型.
        4) 垃圾回收算法.
            复制算法:
                针对于年轻代.
                
            标记清除算法:
            标记整理算法:
                针对于老年代
        5) JVM垃圾回收器.
            Serial单线程.
            ParNew多线程.
            Parallel Scavenge: 并发多线程.
            CMS: 以获取"最短垃圾回收停顿时间"为目标的收集器.
            G1: JDK1.8出现的, JDK1.9被设置成默认垃圾回收器.
        6)     JVM调优工具:
            jstat, jmap, jstack, 可视化调优工具(jvisualvm.exe).
        
        
    //以下内容是为了面试用, 找工作前一周, 看看下面的题即可.
    21) JVM的线程安全与锁的两种方式
        线程安全:
            多线程, 并发, 操作同一数据, 就有可能引发安全问题, 需要用到"同步"解决.
            
        "同步"分类:
            同步代码块:
                格式:
                    synchronized(锁对象) {
                        //要加锁的代码
                    }
                注意:
                    1) 同步代码块的锁对象可以是任意类型的对象.
                        //对象多, 类锁均可.
                    2) 必须使用同一把锁, 否则可能出现锁不住的情况.                //String.class
                    
            同步方法:
                静态同步方法:
                    锁对象是: 该类的字节码文件对象.        //类锁
                    
                非静态同步方法:
                    锁对象是: this                        //对象锁
        
    22) 脏读-高圆圆是男的
        1) 演示main.java.thread.DirtyRead.java类的代码即可.
        
        2) 自定义线程修改姓名后, 要休眠3秒, 而主线程休眠1秒后即调用getValue()打印姓名和年龄,
           如果getValue()方法没加同步, 会出现"脏读"的情况.
           
    23) 了解Lock锁.
        1) Lock和synchronized的区别
             1.1) synchronized是java内置的语言,是java的关键字
            1.2) synchronized不需要手动去释放锁,当synchronized方法或者synchronized代码块执行完毕。
                系统会自动释放对该锁的占用。
                而lock必须手动的释放锁,如果没有主动的释放锁,则可能造成死锁的问题
        2) 示例代码
            public class Demo02 {
                private Lock lock = new ReentrantLock();

                public void method01() {
                    lock.lock();
                    System.out.print("i");
                    System.out.print("t");
                    System.out.print("c");
                    System.out.print("a");
                    System.out.print("s");
                    System.out.print("t");
                    System.out.println();
                    lock.unlock();
                }


                public void method02() {
                    lock.lock();
                    System.out.print("我");
                    System.out.print("爱");
                    System.out.print("你");
                    System.out.print("中");
                    System.out.print("国");
                    System.out.println();
                    lock.unlock();
                }
            }

        


    https://blogs.oracle.com/jonthecollector/our-collectors
      
        
            


            
            
            
            
            
            
            
            
            
        
       

  • 相关阅读:
    Why Visual Studio Team System Isn't A LoadRunner Killer[转载]
    从键盘判断电脑主人
    LoadRunner中添加weblogic监视器(JMX)
    winrunner事务概念的代码应用(毫秒级)
    Delphi中的线程类
    换工作时如何提高自身的待遇(转贴)
    牛人是怎样用函数实现本地化测试的
    肯德基的见闻
    由 12306.cn 谈谈网站性能技术
    20个优秀的 CSS 网格系统(CSS Grid Systems)推荐
  • 原文地址:https://www.cnblogs.com/jjpbk/p/11728459.html
Copyright © 2011-2022 走看看