Java新瓶装旧酒---关于JVM的理解

127 阅读8分钟
Java关于JVM这块也算是老生常谈了,初学的时候感觉很晦涩难懂,一直没彻底搞明白,面试的时候总是会掉坑,最近刚好有空,

又把JVM的重新温故了一遍,突然发现豁然开朗。下面谈谈我的对JVM的理解。

1.JVM这东西什么作用?
    实际上JVM的作用和设计实际上是为了解耦,解的是操作系统和我们应用程序之间的耦合关系,因为在没有JVM的时候,在应用操作系统上的一些高级特性时,比如线程,IO等等操作时,每一套操作系统实现这些特性的方法是不同的,如何我们要兼容每个平台,就需要我们自己对不同的平台做兼容(编写不一样的代码)。JVM推出之后,我们只需要一套API接口就能打天下。这也是Java受欢迎的原因。
 
    除了上面的好处之外,JVM可以帮助我们自动进行垃圾回收。至于怎么进行垃圾回收,我们后面再细细的说。
     
     大家都知道java程序是通过 javac 命令先把 A.java文件 转变成 A.class之后,通过 java A 执行. java是怎么运行这个A.class这个文件的。 这里不得不提一个重要的类 ClassLoader。
 
2.关于ClassLoader一个重要的知识点自然是双亲委派机制和沙箱安全机制: 
    java程序运行期间,默认存在3种ClassLoader:
        1.BootstrapClassLoader
        2.ExtensionClassLoader
        3.AppClassLoader
        
        每一种ClassLoader都对应了自己的加载范围
           BootstrapClassLoader的加载范围是lib文件夹下的rt.jar
           ExtensionClassLoader的加载范围是lib文件夹下的ext/*jar
           AppClassLoader的加载范围是自己的classPath路径下的类。
           
    BootstrapClassLoader做为系统的根classLoader 是 ExtensionClassLoader的父亲,
    ExtensionClassLoader的子ClassLoader是AppClassLoader. ClassLoader这个类,有一个parentClassLoader属性.可以指向
    父ClassLoader。 这样就是形成了一条责任链。是一种责任链设计模式的体现。
        而类加载是采用一种双亲委派的形式,即加载一个类会从根结点开始,如果根节点找不到,再依次下发。
    这也使得系统的中的类优先加载,不会被外部定义的重名类型破坏,保证了JVM系统的安全性,也是一种沙箱安全的体现。
    当磁盘上的.class文件通过类加载器被JVM变成内存对象。(ClassLoader的作用实际上就是把磁盘上的.class文件中的信息存到内存中,这样信息就可以被CPU来执行)。
    
3.那么JVM是怎么把这些信息放到内存中的呢?
    这里不得不提JVM的运行时数据区了。

    JVM运行时把内存划分为 5个部分: 
        1.程序计数器
        2.虚拟机栈
        3.堆
        4.本地方法栈
        5.方法区
   
    一个个来解释:
    1.程序计数器
        首先,什么是程序计数器,为什么需要程序计数器?程序计数器是为了实现线程的上下文切换而定义的内存,因为程序计数器中存储的去当前执行的字节码指令和行号。存储的目的就是为了当currentThread获得了执行权时方便还原当前线程执行的场景。这东西也肯定是线程独占的。线程中存在的区域。
    
    2.虚拟机栈: 其实就是栈空间,栈中就是为了给函数调用准备的。一个函数调用另一个函数时,总是先调用的函数最后返回结果。最后出栈。一个方法对应一个栈帧。一个栈帧中包含:局部变量表,操作数栈,动态链接,方法出口....
    操作数栈是根据字节码指令把局部变量表中的数据,压入栈中进行运算。动态链接:常量池中的符号(名称和描述符)指向真正的内存地址。例如多态,这个链接只有在用到时才会做。方法出口记录return回去的地址(正常出口),还有异常处理的出口。
    
    3.本地方法栈:类似于虚拟机栈操作的是native方法。
    
    4.堆:线程共享区: (线程安全问题的由来)(垃圾回收的主要场所)
    
    5.方法区:存储是已被虚拟机加载的类信息,常量,静态变量,JIT(及时编译后的代码)。 metaspace和永久代都是方法区的具体实现。
    
补充:运行时常量池:当.class文件被Classloader加载之后,class文件中的常量就被放入了常量池中,而且运行期间常量池中也可以加入常量,
如String.intern()方法:
     (如果常量池中存在就返回常量池中的信息,如果不存在就加入到常量池中)
    
4.用完之后怎么被收回呢?
    1.哪些对象应该被回收?
        关于回收算法:存在2种
           1.引用计数器法
           2.GC可达分析法
    通过都采用第二种方法,第一种算法是存在一个问题,就那就是对象如果循环引用, 就会导致内存泄露。
    
    GC可达分析法:是指对象从GCRoot开始,有路径可以达到该对象.就表示该对象不应该被回收,哪些可以作为GCRoot? 局部变量表中引用的对象,本地方法栈中JNI引用的对象,常量和静态变量引用的对象。
    
    2.对象如何被收回?
      1.一共有3种方式:
        1.1复制回收算法
        1.2标记-清除算法
        1.3标记-整理算法
     
    3.为了方便回收内存我们对堆又重新做了划分.因为对象的生命周期是不同的,在生成环境的90%的对象都是朝生夕死的。用完就会被释放掉。所以我们会设置3个区域 分别是新生代(young generation)养老区(tenured generation)1.8之后 meta space:no-heap(非堆)
     
     新生代被划为 8:1:1, 分别是Eden,S0,S1 90%的空间可以被利用,划为区域的目的是因为新生代需要使用复制回收算法,该算法特别高效,不会产生内存碎片的问题。
     对象被分配首先都会在Eden区,Eden区满了之后会触发一次 minorGC:复制回收算法。把剩余存活的对象,都移动到S0中,清除Eden中全部数据。如果下次Eden又慢了,触发MinorGC
     把S0+Eden存活的对象移到S1中.把S0和Eden区情空,默认是对象的年龄达到15岁,就会被移动到(Tenured generation) 中
     如果某个新生的对象超过了阈值,PretenureSizeThreshold,直接会被移到老年代。JVM还有种动态年龄判定的方式:
     相同年龄对象大小的总和>survior区的一半。进入Tenured generation
     
     
    4.新生代到老年代晋升条件:
       1.长期存在的对象,经历了数个垃圾回收阶段之后,大于某个阈值就会晋升到老年代。
       2.如果个某个对象过大,超过了某个阈值(-XX:PreTenuredSizeThrehold)则直接会被分配到老年代,不会走新生代。
       3.JVM有个动态年龄的判断,相同年龄对象大小的总和>survior空间的一半,也会晋升到老年代
       
    5.垃圾收集器:
    
    新生代的垃圾收集器有:Serial,ParNew,Parallel Scavenge.
    
    Serial:单线程的,多用在客户端,用在核数较少的机器上。垃圾线程开始工作的时候就是 STW。(Stop the world)就会停止用户线程工作。

    ParNew:原理同Searial,用多线程实现的
    
    Parallel Scavenge:关注点在吞吐量,所谓的吞吐量就是: 
        用户线程的时间/ 用户线程执行的时间+垃圾回收的时间。
        -XX:MaxGCPauseMillis: 设置最的停顿时间, 设置这个值GC会通过
        调整堆内存的大小和其他参数类满足这个停顿时间,有可能造成GC变得更频繁,降低吞吐量
        -XX:GCTimeRadio: 如果设置为 -XX:GCTimeRadio=19 则垃圾回收的时间为 1/20。  
   |
   |
   老年代的垃圾收集器:
    
    1.CMS:回收停顿的时间会很短,用的是标记-清除的算法 
        1.初始标记: 到达安全点(STW)              initial  mark
            标记老年代中GCRoots,被新生代引用的对象。
        2.并发标记  用户线程和垃圾收集器并发执行  concurrent mark
            会标记所有的存活的对象。如果说对象的引用发送了改变,则被标记为dirty。
        3.重新标记    到达安全点(STW)   remark
            处理那些被标记成dirty的对象,判断是不是存活对象。是就进行标记,不是去掉标记
        4.并发清除        cocurrent sweep
           清除掉没有被标记的对象。
    
    缺点:1.耗费CPU性能
         2.无法清除浮动垃圾
         3.预留一部分老年代空间给用户线程 
         4.碎片化严重
         5.出现concurrent mode failure
    
    2.Serial Old:
        Serial old 是Serial的新生代的老年版,使用的是标记-整理算法。
    3.Parallel Old:
        Parallel Scavenage的老年代版本,使用的也是标记-整理的算法。
    用户线程会停在save point的点,进行垃圾收集操作。
    
    
    G1垃圾收集器