A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

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.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:\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();
            }
        }

2 个回复

倒序浏览
有任何问题欢迎在评论区留言
回复 使用道具 举报
或者添加学姐微信
DKA-2018
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马