• JVM入门


     JVM偏理论,主要靠背 面试题


    JVM的位置

      JVM在操作系统之上,和其他的应用软件层级并列,在之上可以跑java程序

      .java => class文件 => 类加载器 Class Loader =>运行时数据区

    JVM 架构图

    类加载器 ClassLoader 

    类加载器细分:

      1. bootStrap - 根  类加载器(jre/ lib/ rt.jar)

      2. ext - 扩展加载器 (jre/ lib/ ext/ ...)

      3. app - 应用程序(系统类) 加载器,最常用

     双亲委派机制

     选择类加载器的顺序:

      bootStrap->ext->app->custom。先找boot,再找ext、app,最后找costom也就是自己写的。都找不到就报错Class Not Found

    好处:

      防止用户乱定义类:比如自定义一个String类,这时会先从BootStrap找,所以不会轮到自定义的String类

    沙箱安全机制

      Java安全模型的核心是 沙箱 sandbox

    组成沙箱的基本组件:

      字节码校验器(bytecode verifier): 确保遵循java语言规范,帮助实现内存保护;核心类不参与校验;

      类加载器:双亲委派机制 + 将代码归入保护域

    Native

    例子:

    private native void start0();    //java的范围达不到了,要调用C++的库了  //会进入本地方法栈,然后调用本地方法接口JNI(Java Native Interface)
    进入本地方法栈,然后调用本地方法接口JNI(Java Native Interface)
    JNI的作用:扩展Java的使用,一般用于驱动硬件

    三种JVM

    Sun公司:HotSpot、OpenJDK

    其他:JRockit、J9VM

    程序计数器PC

    每一个线程私有的,指向下一条指令的地址

    PC寄存器:仅存一个指针,占用空间非常小

    方法区(内含常量池)

    方法区被所有线程共享,

    方法区含有:静态变量static、常量final、类信息(构造方法、接口定义)、运行时的常量池

    =>  static + final + Class + 常量池

    (堆:实例变量)

      每一个方法内部结构 =》栈帧

    栈内存的生命周期 和线程同步

    线程结束,栈内存就释放了;所以对于栈来说,不存在GC垃圾回收

    栈的内容:8大基本类型 + 对象引用 + 实例的方法

    栈溢出:StackOverflowError 是比较严重的错误


    Heap,一个JVM只有一个堆内存,

    堆内存的大小是可以调节的

      =》 这两个用于调参

    堆中的内容:

      实例对象中的 方法、常量、变量

    堆内存细分为3个区域:

      新生区 young 

      老年区 old

      永久区 perm

    新生区-伊甸园 ,经过轻GC,到幸存区,再经过重GC,到老年区;

    层层筛选后,等老年区也满了,就会OOM内存溢出

    经验:99%的对象都是临时对象。

    新生区

    老年区

    永久区(元空间)

    ==》元空间在逻辑上存在,但是在物理上是不存在的

    元空间,常驻内存。关闭JVM会释放这个区域的内存。

    用来存放JDK自身携带的Class对象,被所有的线程、对象共享;

    Interface元数据,存储的是Java运行时的一些环境or类信息,这个区域不存在垃圾回收

    永久区OOM:

      当一个启动类,加载了大量的第三方jar包;

      Tomcat部署了太多的应用;

      大量动态生成的反射类

    JVM调优

    常用JVM调优参数

    堆内存调优

    public class Test {
        public static void main(String[] args) {
            long max = Runtime.getRuntime().maxMemory();
            System.out.println("max = " + max/1024/1024 + "MB");
            long total = Runtime.getRuntime().totalMemory();
            System.out.println("total = " + total/1024/1024 + "MB");
            //默认max=1/4的总内存; total=1/64的总内存
        }
    }

    这台电脑的内存为32GB

     Configuration里面的VM Options设置为:

    -Xms1024m -Xmx1024m -XX:+PrintGCDetails

      jvm参数非常非常多,可以到网上去查到

      这里的总空间=新生区+老年区,不包括逻辑上的元空间

    制造OOM:

    -Xms2m -Xmx2m -XX:+PrintGCDetails  ##设置堆的大小为2m
    public class Test {
        public static void main(String[] args) {
            long max = Runtime.getRuntime().maxMemory();
            System.out.println("max = " + max/(double)1024/1024 + "MB");
            long total = Runtime.getRuntime().totalMemory();
            System.out.println("total = " + total/(double)1024/1024 + "MB");
            //默认max=1/4的总内存; total=1/64的总内存
    
            String str = "123";
            while(true){
                str+="111";
            }
        }
    }

    解决OOM:

    1)扩大堆内存,试下能不能解决

    2)内存快照分析工具

     Jprofiler 作用:

    • 分析Dump内存文件,快速定位内存泄漏
    • 获得堆中的数据
    • 获得大的对象

     插件

     设置dump导出错误信息:

    -Xms1m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
    public class Test {
        public static void main(String[] args) {
            long max = Runtime.getRuntime().maxMemory();
            System.out.println("max = " + max/(double)1024/1024 + "MB");
            long total = Runtime.getRuntime().totalMemory();
            System.out.println("total = " + total/(double)1024/1024 + "MB");
            //默认max=1/4的总内存; total=1/64的总内存
    
            byte[] array = new byte[1*1024*1024];  //1M
    
            ArrayList<Test>list = new ArrayList<Test>();
            int count = 0;
    
            try {
                while(true){
                    list.add(new Test());   //问题所在
                    count++;
                }
            } catch (Error e) {
                System.out.println("count = " + count);
                e.printStackTrace();
            }
    
        }
    }

    执行上述程序,生成:Dumping heap to java_pid3044.hprof

    1)双击打开,查看

     就知道哪里导致占用大量内存的问题了

    2)点击 

     

    GC:

    垃圾回收主要针对堆,  (少量方法区)

    堆空间

    主要划分为:新生区、老年区、永久区

    新生区分为:伊甸园区、幸存区(两半,0区+1区,轮流倒)8:1:1         (8:1:1比例可以调,留存率高就降低伊甸园区大小)

    需要回收:死对象

    判断对象生死?两个方法:

    1)引用计数器法:

    给对象添加一个引用计数器,每当被引用时加 1,失效时减 1,任何时刻计数器为 0 的对象是不可能再被引用的。

    问题:如果两个对象循环引用,则计数器永远不为0,无法回收。

    2)可达性分析算法:

    每个对象,抽象为一个Node节点,

    从一系列GC Roots Set 中的根出发,不断遍历,如果没有任何引用相连,则不可达。

    (这样可以解决循环引用问题)

    Java中4种引用类型: 

    JDK1.2 之后,将引用分为:强引用、软引用、弱引用、虚引用,强度依次减弱。

    总结:强引用只要还在就不会回收,软引用会在溢出前回收,弱引用在下次回收时回收,虚引用仅会在回收时发送通知。

    强引用(最常用):

      普遍存在,类似 Object obj = new Object(),只要强引用还存在,GC 就无法回收它。

    软引用:

      描述一些还有用但非必须的对象。系统快发生溢出前,会把这些对象列进回收范围之中进行第二次回收。SoftReference 类来实现。

    弱引用:

      描述非必须对象,此类对象只能生存到下一次垃圾回收发生之前,无论内存是否够,都会回收。WeakReference 类实现。

    虚引用:

      唯一目的是能在这个对象被GC时收到一个通知,不会影响对象生存时间也不会获得对象实例。PhantomReference 类实现。

     

    GC  4个常用算法

    1)引用计数法:

    缺点:计数器本身损耗很多性能,效率不高

    2)复制算法:

      ==》适用于复制率较低的 新生区(含:伊甸园区+2个幸存区)

    两个幸存区轮流倒,from=>to,谁空谁是to

    每次GC后,Eden区、from-幸存区 都是被倒空的

    当1个对象经历15次GC,都还没有死,就会荣升 老年区

    优点:没有内存的碎片,速度快

    缺点: to区永远是空的,浪费了不少内存空间

    3)标记-清除法(两轮)

    flag标记是否存活,一次扫描是否存活、一次扫描清除死的对象

    缺点:两次扫描浪费时间;内存碎片

    优点:不需要额外的空间(相对于复制算法)

    4)标记-整理法(标记+清除+压缩)(三轮)

    防止内存碎片:增加一轮扫描,将对象移到一端;

     

    5)分代收集算法

    新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

    老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

    大部分情况对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 Survivor 区,对象年龄加 1,当年龄增加到一定默认 15 岁时,就会被晋升到老年代中。同时,大对象将直接进入老年代。

    3维度比较——时间、空间、碎片

    (下面均是越左越好)

    时间消耗:复制算法 < 标记清除 < 标记压缩

    内存碎片:复制算法 = 标记压缩 < 标记清除

    内存利用率:标记压缩=标记清除 > 复制算法

    GC:分代收集算法

    新生代GC(轻GC):(Minor GC)(伊甸园=》幸存区)

      存活率低、复制率低;轻GC非常频繁

      => 复制算法(15次没死  进入老年区)

    老年代GC(重GC):(Major GC/Full GC)(幸存区=》老年区)

      区域大,存活率高;

      => 标记清除+标记压缩,混合实现(几次清除,一次压缩)

    (老年区满了OOM)解决:

    1)扩大堆内存,试下能不能解决

    2)内存快照分析工具Jprofiler

    垃圾收集器

    主要5种: Serial、ParNew、Parallel Scavenge、CMS、G1

    Serial:

    单线程;简单而高效(与其他收集器的单线程相比);

    新生代采用复制算法,老年代采用标记-整理算法。

    ParNew:

    ParNew 收集器,其实就是 Serial 的多线程版本

     

    Parallel Scavenge:

    几乎和 ParNew 一样,区别是:优化了CPU利用率,吞吐量提高了。

    可设置 Parallel 收集器+ 老年代串行/老年代并行。

    CMS:

    是一个很好的收集器。GC的时候,用户线程几乎不停顿。

    它是基于“标记-清除”算法。

    优点:并发收集、低停顿

    缺点:它用了“标记-清除”算法,会导致空间碎片

    4 个步骤

    1.初始标记暂停其他线程,并记录下直接与 root 相连的对象,速度很快。

    2.并发标记:  GC 和用户线程,同时开启,记录可达对象(标记法、可达法)

    3.重新标记暂停其他线程,更新上一阶段变动的。

    4.并发清除开启用户线程,同时 GC清理

     

    G1:(Garbage-First)

    基于“标记-整理”算法

    优先回收  价值最大 Region

    它针对:多处理器、大内存

    G1 主要有并行与并发、分代收集、空间整合、可预测的停顿的特点。运作大致分为初始标记、并发标记、最终标记、筛选回收这四个过程。

  • 相关阅读:
    计算机操作系统之进程管理
    剑指offer——两个链表的第一个公共结点
    剑指offer——数字在排序数组中出现的次数
    剑指offer——二叉树的深度与平衡二叉树的判断
    剑指offer——数组中只出现一次的数字
    剑指offer——和为s的两个数字VS和为s的连续正数序列
    剑指offer——翻转单词顺序VS左旋转字符串
    剑指offer——扑克牌的顺子
    剑指offer——圆圈中最后剩下的数字
    剑指offer——求1+2+...+n
  • 原文地址:https://www.cnblogs.com/qyf2199/p/14732006.html
Copyright © 2020-2023  润新知