zoukankan      html  css  js  c++  java
  • jvm学习

    JVM是Java Virtual Machine(Java虚拟机)的缩写,运行在操作系统之上。

    JVM体系结构图:

    类装载器:
      负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的
    运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
    例如加载一个Car的class文件:

     类装载分为两类:虚拟机自带和用户自定义

    虚拟机中自带了三种虚拟机:

    • 启动类加载器(Bootstarp):加载lib/rt.jar下的文件
    • 扩展类加载器(Extension):加载lib/ext/*.jar下的扩展文件
    • 应用程序类加载器(AppClassLoader)也叫系统加载器:加载classPath下的所有类,如我们自定义的car类

    demo:

    class  MyObject {
    
    }
    public class ClassLoaderDemo {
        public static void main(String[] args) {
            Object object = new Object();
            //null  Bootstrap 类加载器
            System.out.println(object.getClass().getClassLoader());
           /* //异常  上面已经没有了,Bootstarp是第一个类加载器
            System.out.println(object.getClass().getClassLoader().getParent());
            //异常
            System.out.println(object.getClass().getClassLoader().getParent().getParent());*/
    
            MyObject myObject = new MyObject();
            //AppClassLoader 类加载器
            System.out.println(myObject.getClass().getClassLoader());
            // Extension  扩展类加载器
            System.out.println(myObject.getClass().getClassLoader().getParent());
            //Bootstarp  类加载器  null
            System.out.println(myObject.getClass().getClassLoader().getParent().getParent());
    
        }
    }

    myObject.getClass().getClassLoader():可以获取到加载器得名字

    myObject.getClass().getClassLoader().getParent():可以得到当前加载器的上一个加载器,当已经获取到Bootstrap加载器,再调用getParent()会报空指针异常;

    类加载器的顺序是从上往下加载

    Bootstarp———》Extension————》AppClassLoader

    双亲委派:

           当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,

    每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈

    自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

          采用双亲委派的一个好处是,比如加载位于rt,jar包中的类java.lang.Object, 不管是哪个加载器加载这个类,

    最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

    本地接口:

      在我们看源码的时候,有时会看到native标记的方法,这就是本地方法,它是和底层硬件进行交互。

    它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载native libraies。

    pc寄存器:

      每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一

    条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

    这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节这块内存区域很小,它是当前线程所执行的

    字节码的行号指示器,字节如果执行的是一个Native方法,那这个计数器是空的。用以完成分支、循环、跳转、异常处理、

    线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。

      简单理解就是告诉计算机程序运行到了哪一步。

    Method Area方法区:

      供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池( Runtime Constant Pool) 、

    字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现是不一样的,

    最典型的就是永久伏(PermGen space,1.7以前版本) 和元空间(Metaspace,1.8版本

    栈stack:

    栈是一种数据结构,遵循先进后出/后进先出的原则,例如弹夹。

    每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,

    每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体

    JVM的实现有关,通常在256K~ 756K之 间,与等于1Mb左右。

    每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕后会自动将此栈帧出栈

    demo:

    class Test{
        public void add(){
            add();
        }
    }
    public class StackDemo {
        public static void main(String[] args) {
            Test test = new Test();
            test.add();
        }
    }

    那么栈里面的结构图是这样:

    它会先执行add()方法,再去执行main()方法,但栈都是有内存空间的,当调用的方法多的话,内存空间就会溢出。

    例如,上面的demo我们是递归调用,结果就栈内存溢出。发生Exception in thread "main" java.lang.StackOverflowError,这是一个error错误。

    堆Heap:

    堆结构:

    新生代+老年代+永久代(1.7之前)

    新生代+老年代+元空间(1.8)

    元空间与永久代之间最大的区别在于:

    永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
     
    新生代:
      新生区是类的诞生、成长、消亡的区域,- 一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:
    伊甸区(Eden space) 和幸存者区(Survivor pace), 所有的类都是在伊甸区被new出来的。幸存区有两个: 0区 (Survivor 0 space,也成为from区) 和1区(Survivor 1 space,也称为to区) 。
    当伊甸园的空间用完时,程序又需要创建对象,
    JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC/young Gc),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象
    移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移那么这个时候将产生Ma iorGC (FullGC),进行养老区的内存清理。若养
    老区执行了Full GC之后发现依然无法进行对象的保存,就会产生00M异常“OutOfMemoryError'
    机的堆内存不够。原因有二:
    (1) Java虛拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
    (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
     
    堆中新生代和老年代的结构:

     MinorGC的过程(复制->清空->互换):

    1: eden、 SurvivorFrom复制到SurvivorTo, 年龄+1

    首先,当Eden区满的时候会触发第一 次GC,把还活着的对象拷贝到SurvivorFrom区, 当Eden

    区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回

    收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋

     值到老年代区),同时把这些对象的年龄+1

    2: 清空eden、 SurvivorFrom

    然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to

    3: SurvivorTo和SurvivorFrom互换

    最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一 次GC时的SurvivorFrom区。 部

    分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold

    决定,这个参数默认是15),最终如果还是存活,就存入到老年代

    性能调优:

     参数简介:

    -Xms:设置初始分配大小,默认为物理内存的“1/6”

    -Xmx:最大分配内存,默认为物理内存的“1/4”

    -XX:+PrintGcDetails,输出详细的Gc处理日志

    idea可以在项目Vm options设置这些参数:

     demo:可以获取到虚拟机的一些信息。

          //处理器核数
           System.out.println(Runtime.getRuntime().availableProcessors());
           //java虚拟机器使用的最大内存量
             Long memroyMax =  Runtime.getRuntime().maxMemory();
             //java虚拟机的内存总量
             Long memroyTotal =  Runtime.getRuntime().totalMemory();
            System.out.println("memroyMax"+memroyMax/(double)1024/1024+"M,  
    memroyTotal "+memroyTotal/(double)1024/1024+"M");

    这样我们就可以写一个堆内存溢出(OOM)的demo

    public class HeapDemo {
        public static void main(String[] args) {
           String str = "yang";
           while (true){
                str += str+ new Random().nextInt(888888888)+new Random().nextInt(9999999);
           }
        }
    }

    打印信息[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space这是一个Error错误。

    Gc日志:

    在上面demo中,如果我们设置了-XX:+PrintGCDetails参数还可以看到Gc日志的详细信息:


    [GC (Allocation Failure) [PSYoungGen: 2040K->504K(2560K)] 2040K->743K(9728K), 0.0036043 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [GC (Allocation Failure) [PSYoungGen: 2331K->483K(2560K)] 2570K->1185K(9728K), 0.0038536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [GC (Allocation Failure) [PSYoungGen: 2391K->360K(2560K)] 4338K->2619K(9728K), 0.0142528 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
    [GC (Allocation Failure) [PSYoungGen: 1645K->360K(2560K)] 7642K->6980K(9728K), 0.0059927 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
    [Full GC (Ergonomics) [PSYoungGen: 360K->0K(2560K)] [ParOldGen: 6619K->3805K(7168K)] 6980K->3805K(9728K), [Metaspace: 3468K->3468K(1056768K)], 0.0141733 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
    [GC (Allocation Failure) [PSYoungGen: 1326K->32K(2560K)] 6378K->6329K(9728K), 0.0007138 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [Full GC (Ergonomics) [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 6297K->3182K(7168K)] 6329K->3182K(9728K), [Metaspace: 3468K->3468K(1056768K)], 0.0152305 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
    [GC (Allocation Failure) [PSYoungGen: 40K->0K(1536K)] 5714K->5674K(8704K), 0.0006500 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [Full GC (Ergonomics) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 5674K->4428K(7168K)] 5674K->4428K(8704K), [Metaspace: 3468K->3468K(1056768K)], 0.0050228 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
    [GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4428K->4428K(9216K), 0.0005205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4428K->4408K(7168K)] 4428K->4408K(9216K), [Metaspace: 3468K->3468K(1056768K)], 0.0092480 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

    YoungGc参数解析:

    Full Gc:

     可以使用java自带的jvisualvm进行监控,在应用位于bin/jvisualvm.exe

    Minor GC和Full GC的区别:

    JVM在进行GC时, 并非每次都对上面三个内存区域一 起回收的,大部分时候回收的都是指新生代。

    因此GC按照回收的区域又分了两种类型,一种是普通GC (minor GC   or young GC),一种是全局GC (major GC or Full GC)

    普通GC (minor GC) :只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以

    Minor GC非常频繁,- -般回收速度也比较快。

    全局GC (major GC or Full GC) :指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少- -次的Minor GC (但.

    并不是绝对的)。Major GC的速度- -般要 比Minor GC慢上10倍以上

    GC收集算法:

    • 复制算法
    • 标记清除
    • 标记压缩

    复制算法:HotSpot JVM把年轻代分为S三部分: 1 个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,-般情况下,新创建的

    对象都会被分配到Eden区(- -些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在

    Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一-定程度时,就会被移动到年老代中。因为年轻代中的对象

    基本都是朝生夕死的(90%以上:), 所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只

    用其中一块,当这一块内存用完,就将还活着的对象复制到另外-块上面。

    特点:复制算法不会产生内存碎片,但会占用空间。用于新生代

    标记清除:算法分成标记和清除两个阶段,先标记出要回收的对象,然后统-回收这些对象。

    特点:不会占用额外空间,但会两次扫描,耗时,容易产生碎片,用于老年代

    示意图:

    标记压缩:和标记清除一样,这里还多了一步压缩

    特点:不会产生碎片,但是会耗时,标记以后还要整理存活对象的引用地址,用于老年代。

    三者对比:

    内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不- -定如此)。

    内存整齐度:复制算法=标记整理算法>标记清除算法。

    内存利用率:标记整理算法=标记清除算法>复制算法。

    可以看出,效率上来说,复制算法是效率高德,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法

    相对来少占用内存,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,标记压缩又比标记/清除多了一个整理内存的过程。

    待更。。。

  • 相关阅读:
    7
    6
    5.1
    5
    C#类库帮助类
    Asp.net 数据库依赖那些事
    C#使用NLog记录日志
    JQuery常用操作实现方式
    常用Sql 标量值函数
    Sql语句查询XML
  • 原文地址:https://www.cnblogs.com/tdyang/p/11923281.html
Copyright © 2011-2022 走看看