欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JVM性能优化简介

程序员文章站 2022-05-03 13:28:55
01. JVM是什么 概述: 大白话: 全称Java Virtual Machine(Java虚拟机), 它是一个虚构出来的计算机, 通过实际的计算机来模拟各种计算机的功能. 专业版: JVM是一个进程, 用来模拟计算单元, 将.class字节码文件转成计算机能够识别的指令. //这里可以联想以前大 ......

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


03. jvm虚拟机类加载机制(一):运行顺序
    首先, 我们先来研究jvm的类加载机制, 类加载机制就是把类给读取出来, 我们来看一下它是如何运行的.
    画图演示:
        jvm底层加载类依靠三大组件:
            bootstrapclassloader    //启动类加载器
                //负责加载: jre\lib\rt.jar        //rt: runtime, 运行的意思
                //windows最早不支持java, 没有jre, 后来sun公司打官司赢了, windows开始默认支持jre.
            extclassloader:            //扩展类加载器
                //负责加载: jre\lib\ext\* 文件夹下所有的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 加载 jre\lib\ext\*.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.*(递归导致的)
        //不设置的话在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:\compile\worker.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:\compile\worker.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