精华内容
下载资源
问答
  • 深入了解JVM

    2020-01-06 21:18:15
    接下来就让我们一点点的来了解JVM吧 Class files 是指.java文件通过javac命令编译后生成的.class文件 类装载器子系统(Class Loader) 简称:类装载器 作用 负责加载class文件,class文件在开头有特定的标识...

    JVM的位置

    JVM是运行在操作系统之上的,它与硬件没有直接的交互
    在这里插入图片描述

    JVM体系结构

    在这里插入图片描述
    接下来就让我们一点点的来了解JVM吧

    Class files

    是指.java文件通过javac命令编译后生成的.class文件
    在这里插入图片描述

    类装载器子系统(Class Loader)

    • 简称:类装载器(类加载器)
    • 作用
      • 负责加载class文件,class文件在开头有特定的标识(cafe babe,当然还有其他的一些判断,这是其中之一),将class文件加载到内存中,并将这些内容转换成方法区的运行时数据结构,Class Loader只负责加载class文件的加载,至于是否能够运行由Execution Engine(执行引擎)决定。
      • 类加载是将class字节码文件实例化成Class对象并进行相关初始化的过程。全小写的class是关键字,用来定义类,首字母大写的Class是所有小class的类(getClass()方法)。
        在这里插入图片描述
    • 结构
      • 3+1
      • 虚拟机自带的加载器
        • ①启动类加载器(BootstrapLoader)
          • 该加载器是用c++语言编写的,我们无法在java代码中获取到该加载器
          • 负责加载的路径:sun.boot.class.path
        • ②扩展类加载器(ExtensionLoader)
          • 该加载器是用java语言编写的,我们可以在java代码中获取到该加载器
          • 负责加载的路径:java.ext.dirs
        • ③应用程序类加载器(AppClassLoader)
          • 该加载器是用java语言编写的,我们可以在java代码中获取到该加载器
          • 负责加载的路径:java.class.path
      • 用户可以自定义类加载器
        • 继承Java.lang.ClassLoader
    • 加载顺序
      • BootstrapLoader会在JVM启动之后载入,之后它会载入ExtClassLoader并将ExtClassLoader的parent设为BootstrapLoader,然后BootstrapLoader再加载AppClassLoader,并将AppClassLoader的parent设定为 ExtClassLoader。
        • JDK中自带的类,他的加载器为BootstrapLoader,我们输出会是null(因为是C语言编写的,无法获取)
        • 我们自己定义的类,加载器为AppClassLoader,其parent为ExtClassLoader,ExtClassLoader的parent为BootstrapLoader
      •   public class ClassLoader {
              public static void main(String[] args) {
                  Object object = new Object();
                  System.out.println(object.getClass().getClassLoader());
                  
                  ClassLoader classLoader = new ClassLoader();
                  System.out.println(classLoader.getClass().getClassLoader().getParent().getParent());
                  System.out.println(classLoader.getClass().getClassLoader().getParent());
                  System.out.println(classLoader.getClass().getClassLoader());
              }
          }
        
    • 双亲委派
      • 当我们加载一个类的时,如果先交给了AppClassLoader,AppClassLoader不会直接去寻找该类,而是先交给其parent:ExtClassLoader,同样ExtClassLoader也会先交给BootstrapLoader,然后BootstrapLoader开始查找该类,如果找到了就由BootstrapLoader来加载该类,如果没找到,那么BootstrapLoader会交给ExtClassLoader来寻找加载,如果ExtClassLoader也没找到,那么交给AppClassLoader来寻找加载,如果AppClassLoader也没找到此时就会抛出异常ClassNotFoundException
      • 和ClassNotFoundException很相似的有一个NoClassDefFoundError
        • ClassNotFoundException:加载类时找不到
        • NoClassDefFoundError:编译时没问题,运行时找不到这个类的定义
      • 简单来说:如果一个类加载器收到了 类载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
      • 采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。
      • 防止内存中出现多份同样的字节码(安全性角度)
        在这里插入图片描述
      • 例如:String这个类是jdk中自带的:java.lang.String,那么如果我们自己也写一个java.lang.String会怎么样呢?
        •   package java.lang;
          
            public class String {
                public static void main(String[] args) {
                    System.out.println("Hello String");
                }
            }
          
        • 创建好之后我们来运行这个程序
          在这里插入图片描述
        • 结果运行时说在java.lang.String中找不到main方法,我们自己写的String类中是有main方法的,那为什么会找不到呢?
        • 因为他最终运行的根本就不是我们自己写的这个String,而是jdk中早已写好的String,最终加载String的时候是由Bootstrap来加载的,这也证明了双亲委派机制

    接下来我们先说简单的,复杂的放到后面说

    执行引擎(Execution Engine)

    负责解释命令,提交操作系统执行。

    本地接口(Native Interface)

    • 本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
    • 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等。
    • 被动接口在java代码中就是那些被native修饰的方法

    本地方法栈(Native Method Stack)

    • 这并不是我们平时说的那个栈
    • 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

    程序计算器(Programma Counter )

    • 我记着这个当年是在汇编语言中讲的
    • 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
    • 它是当前线程所执行的字节码的行号指示器
    • 这块内存区域很小,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

    方法区(Method Area)

    • 供各线程共享的运行时内存区域。

      • 方法区存储的是每个class的信息:
        • (1)类加载器引用(ClassLoader)
        • (2)运行时常量池:包含所有常量、字段引用、方法引用、属性
        • (3)字段数据:每个字段的名字、类型(如类的全路径名、类型或接口) 、修饰符(如public、abstract、final)、属性
        • (4)方法数据:每个方法的名字、返回类型、参数类型(按顺序)、修饰符、属性
        • (5)方法代码:每个方法的字节码、操作数栈大小、局部变量大小、局部变量表、异常表和每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
    • 方法区可以理解为是一种规范,在不同虚拟机里头实现是不一样的,最典型的就是JDK1.7中的永久代(PermGen space)和JDK1.8中的元空间(Metaspace)。

    • 注意: 方法区中存的是类的结构信息,不是实例变量,变量的引用在栈中,new 出来的对象在Java堆中

      •   String str = new String();
          //str在栈中,new String()在堆中
        

    Java栈(Java Stack)

    • 也就是我们平时说的栈
      • 栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,他的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。
      • 8种基本数据类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配
    • 栈存储什么?
      • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量
      • 栈操作(Operand Stack):记录出栈、入栈的操作
      • 栈帧数据(Frame Data):包括类文件、方法等等
    • 栈的特点
      • 先入后出
        没找到合适的java的动图,找了一张JavaScript的,栈的原理都是一样的
        在这里插入图片描述

    栈+堆+方法区的交互关系

    在这里插入图片描述
    JDK是使用指针的方式来访问对象:Java堆中会存放访问类元数据的地址,reference存储的就直接是对象的地址

    堆(Heap)

    • 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
    • 堆内存结构:
      • java8之前
        • 新生区+养老区+永久区(有的也称为新生代、养老代、永久代)
          在这里插入图片描述
      • java8及其以后将永久区改为元空间
        • 此时
          • 堆在逻辑上包含:新生区、养老区、元空间
          • 堆在实际上包含:新生区、养老区
        • 元空间实际上时间本地内存中,并不在堆中,但我们口头上依旧习惯将元空间放到堆中
        • 将元空间从堆中挪出去主要是为了加大元空间的内存容量,尽量减少内容溢出 在这里插入图片描述
        • 元空间放置类信息:字段、静态属性、方法、常量等
    • 新生区
      • 新生区又分为三个区
        • 伊甸区
        • 幸存0区
        • 幸存1区
      • 例如:
        •   String str = new String();
            //此时的new String()是诞生在堆内存中的新生区的伊甸区
          
    • 对象在堆中的移动
      • 新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
      • 如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
        • ①Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
        • ②代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
    • 从GC的角度来看
      在这里插入图片描述
      • ①eden、SurvivorFrom 复制到 SurvivorTo,年龄+1 (幸存区又被称为from区和to区,谁空谁是to)
        • 首先,当Eden区满的时候会触发第一次GC,当GC线程启动时,会通过可达性分析法(对于用可达性分析法搜索不到的对象,GC并不一定会回收该对象。要完全回收一个对象,至少需要经过两次标记的过程)把Eden区和From Space区的存活对象复制到To Space区,同时把这些对象的年龄+1,然后把Eden Space和From Space区的对象释放掉。当GC轮训扫描To Space区一定次数后(15次),把依然存活的对象复制到老年代
      • ②清空 eden、SurvivorFrom
        • 然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to
      • ③SurvivorTo和 SurvivorFrom 互换
        • 最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
      • ④大对象特殊情况
        • 如果分配的新对象比较大Eden区放不下但Old区可以放下时,对象会被直接分配到Old区(即没有晋升这一过程,直接到老年代了)
      • 经研究,不同的对象生命周期不同,98%的对象是临时对象(活不过一次GC)
        在这里插入图片描述
      • 新生区为啥需要Survivor区
        • 不就是新生代到老年代么,直接Eden到Old不好了吗?为啥要这么复杂?想想如果没有Survivor区,Eden区每进行一次MinorGC存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次MinorGC没有消灭但其实也并不会蹦跶多久,或许第2次第3次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。所以Survivor的存在意义就是减少被送到老年代的对象,进而减少FullGC的发生。Survivor的预筛选保证只有经历15次MinorGC还能在新生代中存活的对象,才会被送到老年代。
        • 新生区为啥需要两个Survivor区
          • 设置两个Survivor区最大的好处就是解决内存碎片化。
          • 假设Survivor如果只有一个区域会怎样?MinorGC执行后Eden区被清空了,存活的对象放到了Survivor区,而之前Survivor区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为Survivor有2个区域,所以每次MinorGC,会将之前Eden区和From区中的存活对象复制到To区域。第二次MinorGC时,From与To职责兑换,这时候会将Eden区和To区中的存活对象再复制到From区域,以此反复。这种机制最大的好处就是,整个过程中,永远有一个Survivorspace是空的,另一个非空的Survivorspace是无碎片的。那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,容易导致Survivor区满,两块Survivor区是经过权衡之后的最佳方案。

    个个区域的线程共享问题

    在这里插入图片描述
    区别于永久代,元空间在本地内存中分配。在JDK8里,Perm 区中的所有内容中字符串常量移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间内
    在这里插入图片描述

    展开全文
  • 深入了解jvm

    2017-10-10 15:50:23
    一、运行时数据区域 Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区 1.1 程序计数器 ...

    转载自http://www.cnblogs.com/prayers/p/5515245.html

     

    一、运行时数据区域

    Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区

    1.1 程序计数器

    程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemotyError情况的区域

    1.2 Java虚拟机栈

    虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    栈内存就是虚拟机栈,或者说是虚拟机栈中局部变量表的部分

    局部变量表存放了编辑期可知的各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(refrence)类型和returnAddress类型(指向了一条字节码指令的地址)

    其中64位长度的longdouble类型的数据会占用两个局部变量空间,其余的数据类型只占用1个。

    Java虚拟机规范对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。如果虚拟机扩展时无法申请到足够的内存,就会跑出OutOfMemoryError异常

    1.3 本地方法栈

    本地方法栈和虚拟机栈发挥的作用是非常类似的,他们的区别是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务

    本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryErroy异常

    1.4 Java

    堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建,此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。所有的对象实例和数组都在堆上分配

    Java堆是垃圾收集器管理的主要区域。Java堆细分为新生代和老年代

    不管怎样,划分的目的都是为了更好的回收内存,或者更快地分配内存

    Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有完成实例分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常

    1.5 方法区

    方法区它用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

    除了Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载

    当方法区无法满足内存分配需求时,将抛出OutOfMemoryErroy异常

    1.6 运行时常量池

    它是方法区的一部分。Class文件中除了有关的版本、字段、方法、接口等描述信息外、还有一项信息是常量池,用于存放编辑期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

    Java语言并不要求常量一定只有编辑期才能产生,也就是可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法

    当常量池无法再申请到内存时会抛出OutOfMemoryError异常

     

    二、hotspot虚拟机对象

    2.1 对象的创建

    1.检查 

    虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

    2.分配内存 

    接下来将为新生对象分配内存,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。

    假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”

    如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”

    选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

    3. Init

    执行new指令之后会接着执行Init方法,进行初始化,这样一个对象才算产生出来

    2.2 对象的内存布局

    HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据和对齐填充

    对象头包括两部分:

    a) 储存对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

    b) 另一部分是指类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例

    2.3 对象的访问定位

    1. 使用句柄访问

    Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址

    优势:reference中存储的是稳点的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

     

     

     

    1. 使用直接指针访问

    Java堆对象的布局就必须考虑如何访问类型数据的相关信息,refreence中存储的直接就是对象的地址

     

    优势:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

     

    三、OutOfMemoryError 异常

    3.1 Java堆溢出

    Java堆用于存储对象实例,只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在数量到达最大堆的容量限制后就会产生内存溢出异常

    如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置

    如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

    3.2 虚拟机栈和本地方法栈溢出

    对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError

    如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

    在单线程下,无论由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常

    如果是多线程导致的内存溢出,与栈空间是否足够大并不存在任何联系,这个时候每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。解决的时候是在不能减少线程数或更换64为的虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程

    3.3 方法区和运行时常量池溢出

    String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

    由于常量池分配在永久代中,可以通过-XX:PermSize-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。

    Intern():

    JDK1.6 intern方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是一个引用

    JDK1.7 intern()方法的实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个

     

    四、垃圾收集

    程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了

    1.判断对象存活

    4.1.1 引用计数器法

    给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的

    4.1.2 可达性分析算法

    通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明此对象时不可用的

    Java语言中GC Roots的对象包括下面几种:

    1.虚拟机栈(栈帧中的本地变量表)中引用的对象

    2.方法区中类静态属性引用的对象

    3.方法区中常量引用的对象

    4.本地方法栈JNINative方法)引用的对象

    2.引用

    强引用就是在程序代码之中普遍存在的,类似Object obj = new Object() 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

    软引用用来描述一些还有用但并非必须的元素。对于它在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存才会抛出内存溢出异常

    弱引用用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象

    虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知

    3.Finalize方法

    任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了

    4.3.1 回收方法区

    永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

    废弃常量:假如一个字符串abc已经进入了常量池中,如果当前系统没有任何一个String对象abc,也就是没有任何Stirng对象引用常量池的abc常量,也没有其他地方引用的这个字面量,这个时候发生内存回收这个常量就会被清理出常量池

    无用的类:

    1.该类所有的实例都已经被回收,就是Java堆中不存在该类的任何实例

    2.加载该类的ClassLoader已经被回收

    3.该类对用的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

    4.垃圾收集算法

    4.4.1 标记—清除算法

    算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象、

    不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

    4.4.2 复制算法

    他将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可

    不足:将内存缩小为了原来的一半

    实际中我们并不需要按照1:1比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

    当另一个Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代

    4.4.3 标记整理算法

    让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

    4.4.4 分代收集算法

    只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收

    5.垃圾收集器

    a)Serial收集器:

    这个收集器是一个单线程的收集器,但它的单线程的意义不仅仅说明它会只使用一个COU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它手机结束

    b)ParNew 收集器:

    Serial收集器的多线程版本,除了使用了多线程进行收集之外,其余行为和Serial收集器一样

    并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

    并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU

    c)Parallel Scavenge 

    收集器是一个新生代收集器,它是使用复制算法的收集器,又是并行的多线程收集器。

    吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

    d)Serial Old 收集器:

    Serial收集器的老年代版本,是一个单线程收集器,使用标记整理算法

    e)Parallel Old 收集器:

    Parallel OldParaller Seavenge收集器的老年代版本,使用多线程和标记整理算法

    f)CMS收集器:

    CMS收集器是基于标记清除算法实现的,整个过程分为4个步骤:

    1.初始标记2.并发标记3.重新标记4.并发清除

    优点:并发收集、低停顿

    缺点:

    1.CMS收集器对CPU资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3/4

    2.CMS收集器无法处理浮动垃圾,可能出现Failure失败而导致一次Full G场地产生

    3.CMS是基于标记清除算法实现的

    g)G1收集器:

    它是一款面向服务器应用的垃圾收集器

    1.并行与并发:利用多CPU缩短STOP-The-World停顿的时间

    2.分代收集

    3.空间整合:不会产生内存碎片

    4.可预测的停顿

    运作方式:初始标记,并发标记,最终标记,筛选回收

    6.内存分配与回收策略

    4.6.1 对象优先在Eden分配:

    大多数情况对象在新生代Eden区分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

    4.6.2 大对象直接进入老年代:

    所谓大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。这样做的目的是避免Eden区及两个Servivor之间发生大量的内存复制

    4.6.3长期存活的对象将进入老年代

    如果对象在Eden区出生并且尽力过一次Minor GC后仍然存活,并且能够被Servivor容纳,将被移动到Servivor空间中,并且把对象年龄设置成为1.对象在Servivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋级到老年代中

    4.6.4动态对象年龄判定

    为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋级到老年代,如果在Servivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入到老年代,无须登到MaxTenuringThreshold中要求的年龄

    4.6.4 空间分配担保:

    在发生Minor GC 之前,虚拟机会检查老年代最大可 用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor DC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么会继续检查老年代最大可用的连续空间是否大于晋级到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次MinorGC 是有风险的:如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC

    五、虚拟机类加载机制

    虚拟机吧描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

    Java语言里面,类型的加载。连接和初始化过程都是在程序运行期间完成的

    5.1 类加载的时机

    类被加载到虚拟机内存中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段

    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以再初始化阶段之后再开始,这个是为了支持Java语言运行时绑定(也成为动态绑定或晚期绑定)

    虚拟机规范规定有且只有5种情况必须立即对类进行初始化:

    1.遇到newgetstaticputstaticinvokestatic4条字节码指令时,如果类没有进行过初始化,则需要触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候

    2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化

    3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

    4.当虚拟机启动时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

    5.当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

    被动引用:

    1.通过子类引用父类的静态字段,不会导致子类初始化

        2.通过数组定义来引用类,不会触发此类的初始化

    3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

    接口的初始化:接口在初始化时,并不要求其父接口全部完成类初始化,只有在正整使用到父接口的时候(如引用接口中定义的常量)才会初始化

    5.2 类加载的过程

    5.2.1 加载

    1)通过一个类的全限定名类获取定义此类的二进制字节流

    2)将这字节流所代表的静态存储结构转化为方法区运行时数据结构

    3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

    怎么获取二进制字节流?

    1)从ZIP包中读取,这很常见,最终成为日后JAREARWAR格式的基础

    2)从网络中获取,这种场景最典型的应用就是Applet

    3)运行时计算生成,这种常见使用得最多的就是动态代理技术

    4)由其他文件生成,典型场景就是JSP应用

    5)从数据库中读取,这种场景相对少一些(中间件服务器)
    数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的

    数组类的创建过程遵循以下规则:

    1)如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,那就递归采用上面的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识

    2)如果数组的组件类型不是引用类型(列如int[]组数)Java虚拟机将会把数组C标识为与引导类加载器关联

    3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public

    5.2.2 验证

    验证阶段会完成下面4个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证

    1.文件格式验证

    第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括:

    1.是否以魔数oxCAFEBABE开头

    2.主、次版本号是否在当前虚拟机处理范围之内

    3.常量池的常量中是否有不被支持的常量类型(检查常量tag标志)

    4.指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

    5.CONSTANT_Itf8_info 型的常量中是否有不符合UTF8编码的数据

    6.Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

    这个阶段的验证时基于二进制字节流进行的,只有通过类这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流

    2.元数据验证

    1.这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)

    2.这个类的父类是否继承了不允许被继承的类(被final修饰的类)

    3.如果这个类不是抽象类,是否实现类其父类或接口之中要求实现的所有方法

    4.类中的字段、方法是否与父类产生矛盾(列如覆盖类父类的final字段,或者出现不符合规则的方法重载,列如方法参数都一致,但返回值类型却不同等)

    第二阶段的主要目的是对类元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息

    3.字节码验证

    第三阶段是整个验证过程中最复杂的一个阶段,主要目的似乎通过数据流和控制流分析,确定程序语言是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

    1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,列如,列如在操作数栈放置类一个int类型的数据,使用时却按long类型来加载入本地变量表中

    2.保证跳转指令不会跳转到方法体以外的字节码指令上

    3.保证方法体中的类型转换时有效的,列如可以把一个子类对象赋值给父类数据类型,这个是安全的,但是吧父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的

    4.符号引用验证

    发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。

    1.符号引用中通过字符串描述的全限定名是否能找到相对应的类

    2.在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

    3.符号引用中的类、字段、方法的访问性是否可被当前类访问

     

    对于虚拟机的类加载机制来说,验证阶段是非常重要的,但是不一定必要(因为对程序运行期没有影响)的阶段。如果全部代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

    5.2.3 准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量都在方法区中进行分配。这个时候进行内存分配的仅包括类变量(static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里说的初始值通常下是数据类型的零值。

    假设public static int value = 123

    那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行,但是如果使用final修饰,则在这个阶段其初始值设置为123

    5.2.4解析

    解析阶段是虚拟机将常量池内符号引用替换为直接引用的过

    5.2.5 初始化

    类的初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才正真开始执行类中定义的Java程序代码(或者说是字节码)

    5.3 类的加载器

    5.3.1 双亲委派模型:

    只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分。另一种是所有其他的类加载器,使用JAVA实现,独立于JVM,并且全部继承自抽象类java.lang.ClassLoader.

    启动类加载器(Bootstrap ClassLoader),负责将存放在<JAVA+HOME>\lib目录中的,或者被-Xbootclasspath参数所制定的路径中的,并且是JVM识别的(仅按照文件名识别,如rt.jar,如果名字不符合,即使放在lib目录中也不会被加载),加载到虚拟机内存中,启动类加载器无法被JAVA程序直接引用。

    扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

    应用程序类加载器(Application ClassLoader),由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

     

    这张图表示类加载器的双亲委派模型(Parents Delegation model双亲委派模型要求除了顶层的启动加载类外,其余的类加载器都应当有自己的父类加载器。,这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父类加载器的代码。

    5.3.2双亲委派模型的工作过程是:

    如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都是应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    5.3.3这样做的好处就是:

    Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱

    就是保证某个范围的类一定是被某个类加载器所加载的,这就保证在程序中同 一个类不会被不同的类加载器加载。这样做的一个主要的考量,就是从安全层 面上,杜绝通过使用和JRE相同的类名冒充现有JRE的类达到替换的攻击方式

     

    六、Java内存模型与线程

    6.1内存间的交互操作

    关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

    lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

    unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

    load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

    assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

    write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

      如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行readload操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行storewrite操作。Java内存 模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是readload之间, storewrite之间是可以插入其他指令的,如对主内存中的变量ab进行访问时,可能的顺 序是read aread bload b, load a

    Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

    不允许readloadstorewrite操作之一单独出现

    不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

    不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

    一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(loadassign)的变量。即就是对一个变量实施usestore操作之前,必须先执行过了assignload操作。

    一个变量在同一时刻只允许一条线程对其进行lock操作,lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lockunlock必须成对出现

    如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行loadassign操作初始化变量的值

    如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

    对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行storewrite操作)。

    6.2 重排序

    在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。重排序分成三种类型:

    1.编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。

    2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    3.内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    Java源代码到最终实际执行的指令序列,会经过下面三种重排序:

     

    为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoadLoadStoreStoreLoadStoreStore四种:

     

    6.3 对于volatile型变量的特殊规则

    当一个变量定义为volatile之后,它将具备两种特性:

    第一:保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。普通变量的值在线程间传递需要通过主内存来完成

    由于valatile只能保证可见性,在不符合一下两条规则的运算场景中,我们仍要通过加锁来保证原子性

    1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

    2.变量不需要与其他的状态变量共同参与不变约束

    第二:禁止指令重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中执行顺序一致,这个就是所谓的线程内表现为串行的语义

     

    Java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,VW分别表示两个volatile变量,那么在进行readloaduseassignstorewrite操作时需要满足如下的规则:

    1.只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load操作。线程T对变量Vuse操作可以认为是与线程T对变量Vloadread操作相关联的,必须一起连续出现。这条规则要求在工作内存中,每次使用变量V之前都必须先从主内存刷新最新值,用于保证能看到其它线程对变量V所作的修改后的值。

    2.只有当线程T对变量V执行的前一个动是assign的时候,线程T才能对变量V执行store操作;并且,只有当线程T对变量V执行的后一个动作是store操作的时候,线程T才能对变量V执行assign操作。线程T对变量Vassign操作可以认为是与线程T对变量Vstorewrite操作相关联的,必须一起连续出现。这一条规则要求在工作内存中,每次修改V后都必须立即同步回主内存中,用于保证其它线程可以看到自己对变量V的修改。

    3.假定操作A是线程T对变量V实施的useassign动作,假定操作F是操作A相关联的loadstore操作,假定操作P是与操作F相应的对变量Vreadwrite操作;类型地,假定动作B是线程T对变量W实施的useassign动作,假定操作G是操作B相关联的loadstore操作,假定操作Q是与操作G相应的对变量Vreadwrite操作。如果A先于B,那么P先于Q。这条规则要求valitile修改的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。

    6.4 对于longdouble型变量的特殊规则

    Java模型要求lockunlockreadloadassignusestorewrite8个操作都具有原子性,但是对于64为的数据类型(longdouble),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作分为两次32为的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的loadstorereadwrite4个操作的原子性

    6.5 原子性、可见性和有序性

    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。Java内存模型是通过在变量修改后将新值同步会主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,valatile特殊规则保障新值可以立即同步到祝内存中。Synchronized是在对一个变量执行unlock之前,必须把变量同步回主内存中(执行storewrite操作)。被final修饰的字段在构造器中一旦初始化完成,并且构造器没有吧this的引用传递出去,那在其他线程中就能看见final字段的值

    可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    有序性:即程序执行的顺序按照代码的先后顺序执行。

    6.6  先行发生原则

    这些先行发生关系无须任何同步就已经存在,如果不再此列就不能保障顺序性,虚拟机就可以对它们任意地进行重排序

    1.程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说,应该是控制顺序而不是程序代码顺序,因为要考虑分支。循环等结构

    2.管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而后面的是指时间上的先后顺序

    3.Volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的后面同样是指时间上的先后顺序

    4.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

    5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.joke()方法结束、ThradisAlive()的返回值等手段检测到线程已经终止执行

    6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生,可以通过Thread.interrupted()方法检测到是否有中断发生

    7.对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

    8.传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

    6.7  Java线程调度

    协同式调度:线程的执行时间由线程本身控制

    抢占式调度:线程的执行时间由系统来分配

    6.8 状态转换

    1.新建

    2.运行:可能正在执行。可能正在等待CPU为它分配执行时间

    3.无限期等待:不会被分配CUP执行时间,它们要等待被其他线程显式唤醒

    4.限期等待:不会被分配CUP执行时间,它们无须等待被其他线程显式唤醒,一定时间会由系统自动唤醒

    5.阻塞:阻塞状态在等待这获取到一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生;等待状态就是在等待一段时间,或者唤醒动作的发生

    6.结束:已终止线程的线程状态,线程已经结束执行

     

    七、线程安全

    1、不可变:不可变的对象一定是线程安全的、无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障。例如:把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。

    2、绝对线程安全

    3、相对线程安全:相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性

    4、线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用

    5、线程对立:是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码

    7.1 线程安全的实现方法

    1.互斥同步:

    同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。互斥是因,同步是果:互斥是方法,同步是目的

    Java中,最基本的互斥同步手段就是synchronized关键字,它经过编译之后,会在同步块的前后分别形成monitorentermonitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有指明,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,对应的在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,哪当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止

    SynchronizedReentrantLock增加了一些高级功能

    1.等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助

    2.公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;非公平锁则不能保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。Synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁

    3.锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()notify()notifyAll()方法可以实现一个隐含的条件,如果要和多余一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition方法即可

    2.非阻塞同步

    3.无同步方案

    可重入代码:也叫纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身)而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。

    判断一个代码是否具备可重入性:如果一个方法,它的返回结果是可预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的

    线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保障,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题

    7.2锁优化

    适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁

    7.2.1 自旋锁与自适应自旋

    自旋锁:如果物理机器上有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程稍等一下,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁

    自适应自旋转:是由前一次在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自过程,以避免浪费处理器资源。

    7.2.2 锁消除

    锁消除是指虚拟机即时编辑器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。如果在一段代码中。推上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行

    7.2.3锁粗化

    如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部

    7.2.4 轻量级锁

    7.2.5 偏向锁

    它的目的是消除无竞争情况下的同步原语,进一步提高程序的运行性能。如果轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把这个同步都消除掉,CAS操作都不做了

    如果在接下俩的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要在进行同步

    八、逃逸分析

    逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,成为方法逃逸。甚至还可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸

    如果一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化

    栈上分配:如果确定一个对象不会逃逸出方法外,那让这个对象在栈上分配内存将会是一个不错的注意,对象所占用的内存空间就可以随栈帧出栈而销毁。如果能使用栈上分配,那大量的对象就随着方法的结束而销毁了,垃圾收集系统的压力将会小很多

    同步消除:如果确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉

    标量替换:标量就是指一个数据无法在分解成更小的数据表示了,intlong等及refrence类型等都不能在进一步分解,它们称为标量。

    如果一个数据可以继续分解,就称为聚合量,Java中的对象就是最典型的聚合量

    如果一个对象不会被外部访问,并且这个对象可以被拆散的化,那程序正整执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替

    作者:刘金辉  战斗民族就是干

    展开全文
  • 深入了解JVM 共60课

    2021-04-30 14:00:26
    深入了解JVM 共60课 (质量超高)
  • 主要介绍了深入了解JVM字节码增强技术,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 1.《深入了解JVM虚拟机》第二版第三版;扫描版;清晰;带书签 2.《高性能MySQL》 第二版第三版;扫描版;清晰;带书签
  • 深入了解JVM垃圾收集机制 文章目录深入了解JVM垃圾收集机制一、哪些内存需要被回收?1、引用计数法2、可达性分析算法3、方法区的回收二、如何进行回收——垃圾收集算法1、标记——清除算法2、标记——复制算法3、...

    深入了解JVM垃圾收集机制


    Java之所以风靡,很大程度上得益于它的垃圾收集机制(Garbage Collection),在大多数时候,Java程序员不再需要像C/C++程序员一样每开辟一块空间都需要去关注他的使用情况以防止内存泄漏,大多数时候垃圾收集器可以帮我们解决内存管理的工作,然而这并不意味着Java不会出现内存泄漏的情况,因此我们有必要去了解垃圾收集机制的原理,当出现内存泄漏的情况可以更快速的进行手动排查。

    想要弄清楚垃圾收集器如何工作,我们需要了解一下几个问题:

    • 哪些内存需要被回收(如何判断一块内存是否可以回收)
    • 何时进行垃圾回收
    • 如何进行回收

    一、哪些内存需要被回收?

    与C/C++中一样,我们需要回收的是那些已经不再使用的内存,那么如何判断一块内存已经不再需要使用?首先需要知到的是垃圾收集行为主要发生在堆中,当然方法区也存在垃圾收集行为,但探讨的重点依然是堆区

    1、引用计数法

    引用计数法的思想十分简单,为每一个对象添加一个引用计数器,当有某某处引用了该对象时,计数器加一,引用失效时则计数器减一,当引用计数器为0时即意味着这一块内存已不再使用可以回收

    引用计数器的缺陷:

    乍一看引用计数法似乎是一个简单并且高效的方法,也不会占用过多额外内存,但它存在着一个致命的缺陷——互相引用(循环引用)。例如下面这段代码,GCTest中有一个Object属性,GCTest的实例a的instance引用了b,而b的instance则引用了a,这样即使执行了a = null;和b = null;两个对象内部依然维持着相互引用,垃圾收集器也没有办法收集这两块区域,因此Java的垃圾收集器没有采用这种方法

    public class GCTest {
        public Object instance;
    
        public static void main(String[] args) {
            GCTest a = new GCTest();
            GCTest b = new GCTest();
            a.instance = b;
            b.instance = a;
            a = null;
            b = null;
        }
    }
    

    2、可达性分析算法

    可达性分析算法又称根搜索方法,其基本思想史使用一些对象作为根节点(GC roots),从GC roots开始向下搜索对象的引用情况,其搜索过的路径称为引用链,若一个对象到所有的GC roots之间都不存在任何引用链,则证明该对象是不可用的。例如下图中,Object1、Object2、Object3与GC roots之间存在引用链,这三个对象均不会被回收,而Object4与Object5之间虽然存在引用,但这两个对象与GC roots之间不存在引用链,因此会被判定为不可用对象

    在这里插入图片描述

    可以作为GC roots的对象:

    • 虚拟机栈中的引用对象
    • 方法区中的类静态属性引用的对象
    • 方法区中的常量引用的对象
    • 本地方法栈中JNI(即native方法)引用的对象

    3、方法区的回收

    在《Java虚拟机规范》中提到过可以不要求方法区实现垃圾收集,也的确存在未实现或未完整实现方法区垃圾回收的收集器,但我们还是有必要了解一下。方法区的垃圾回收性价比是比较低的,其回收的主要内容是废弃的常量和不再使用的类型。

    判定一个常量是否废弃,只需要查看是否还有任何字符串对象引用了常量池中的该常量,以及虚拟机中是否有其他地方引用这个字面量,若均没有,且垃圾收集器判断有必要的情况下,该常量就会被回收

    判定一个类是否不再使用则较为复杂,需要同时满足以下三个条件:

    • 该类所有的实例都已经被回收,堆中不存在该类及其任何派生子类的实例
    • 该类的类加载器已被回收,这个条件除非是经过精心设计的可替换类加载器的场景,否则很难达成
    • 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

    二、如何进行回收——垃圾收集算法

    1、标记——清除算法

    标记——清除算法是最为简单的垃圾收集算法,分为标记与清除两个阶段。标记阶段会将所有可回收的对象进行标记,标记完成后统一回收所有被标记的对象

    在这里插入图片描述

    缺点:

    • 效率低,在标记和清除两个阶段效率都不高
    • 易产生内存碎片,在清除过后会存在大量的不连续空间

    2、标记——复制算法

    其基本思想是:将内存空间划分为大小相等的两个部分,每次只使用其中一个部分,当这一块部分已满时,则将存活的对象复制到另一块区域,并清空原来的内存空间。显然没一次进行垃圾收集时都会通过复制对内存进行整理,完美的解决了标记——清除算法易产生内存碎片的问题

    在这里插入图片描述

    **缺点:**可用内存被缩小为了原先的一半

    目前商用Java虚拟机大多都优先采用标记复制算法。事实上新生代(新生代下面会讲)中的大多数对象都存活不过第一轮,因此标记复制算法采用的两个区域并不需要按1:1的内存比例来设计

    3、标记——整理算法

    复制算法虽然解决了内存碎片的问题,但在对象存活率较高的情况下,每次清理就会进行大量的复制操作从而降低清理效率,并且也会浪费大量的内存空间,因此应运而生了标记——整理算法

    标记——整理算法的思想也十分简单,与标记——清楚算法相似的地方是都需要先对对象进行标记,标记出可回收的内存区域,不同点在于,当标记完成后不会对标记的对象进行清理,而是让存活对象向一端移动,所有对象都移动完成后清理掉边界以外的内存

    在这里插入图片描述

    三、何时进行回收

    1、触发垃圾回收的条件

    • 当应用程序空闲时,即没有应用线程在运行时,GC会被调用。垃圾收集器所在的线程优先级最低,因此当应用忙时,GC线程一般不会被调用,当然内存不足时除外
    • 堆内存不足,即无法创建新的对象时,JVM会强制调用GC线程。若一次垃圾回收之后依然无法满足内存分配的要求,JVM会进行第二次GC,若仍无法满足要求,则抛出OutOfMemoryError异常
    • 调用System.gc()方法,调用System.gc()方法并不会触发垃圾收集,该方法并不会触发垃圾收集,而是一种提醒或者建议,建议JVM进行一次垃圾收集行为,调用System.gc并不会保证垃圾收集行为一定发生,何时进行垃圾收集行为总是取决于JVM

    2、进一步了解引用

    无论是引用标记算法还是可达性分析算法,都是围绕引用吧 来进行讨论的,那么我们就有必要更深入的了解一下引用

    在jdk1.2之前,引用被定义为:若reference类型的数据中存储的是另一块内存的起始地址,则这一块内存酒杯称为引用。而在jdk1.2之后,引用被详细分为了强引用、软引用、弱引用和虚引用四种以更灵活的对内存进行管理

    在这里插入图片描述

    • 强引用:强引用即我们最常见的类似于“Object o = new Object()"的引用。如果一个对象具有强引用,它就不会被垃圾回收器回收,即使出现内存不足的情况,JVM会抛出OutOfMemoryError异常,而不会去回收具有强引用对象。如果想要中断强引用与对象实例的关联,可以将引用赋值为null
    • 软引用:软引用被用来描述那些还有用但非必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围之内并进行第二次回收。若这次回收依然没有足够的内存才会抛出内存溢出异常。jdk1.2之后使用WeakReference来实现软引用
    String str = "hello";
    //软引用的创建
    SoftReference<String> softReference = new SoftReference<String>(str);
    //通过软引用获取对象
    String str2 = softReference.get();
    
    • 弱引用:弱引用的强度比软引用更弱一些,同样用来描述非必须的对象,被软引用关联的对象只能生存到下一次垃圾回收发生之前,当垃圾收集器工作时,无论当前内存是否充足,弱引用对象都会被回收。jdk1.2之后使用WeakReference来实现弱引用
    String str = "hello";
    //软引用的创建
    WeakReference<String> weakReference = new WeakReferencee<String>(str);
    //通过软引用获取对象
    String str2 = weakReference.get();
    
    • 虚引用:虚引用是最弱的一种引用关系,一个对象是否存在虚引用并不影响其生存时间,也无法通过虚引用来获得一个对象实例。虚引用的目的只是希望当这个对象被回收时可以收到一个系统通知。jdk1.2之后使用PhantomReference来实现虚引用
    • 引用队列:引用队列用于保存被回收后对象的引用,可以配合软引用、弱引用和虚引用使用,对引用进行监控。(其中虚引用必须和引用队列一起使用,而引用队列对于弱引用与软引用则不是必要的)若引用队列与引用配合使用,当发生垃圾收集行为,被回收掉的对象对应的软引用、弱引用和虚引用就会被加入到引用队列中

    例如下面这段代码,我们创建四个String对象,分别对应强应用、软引用、弱引用和虚引用,其中后三者使用引用队列进行创建,然后将str2、str3和str4释放,调用System.gc()尝试进行一次垃圾收集,垃圾收集完成后从引用队列中取出指向的对象已被回收的引用

    public class GCTest extends Object {
        public static void main(String[] args){
            String str1 = new String("hello");
            String str2 = new String("hello");
            String str3 = new String("hello");
            String str4 = new String("hello");
            ReferenceQueue<String> queue = new ReferenceQueue<>();
            SoftReference<String> softReference = new SoftReference<String>(str2,queue);
            WeakReference<String> weakReference = new WeakReference<String>(str3,queue);
            PhantomReference<String> phantomReference = new PhantomReference<String>(str4,queue);
            str2 = null;
            str3 = null;
            str4 = null;
            System.gc();
            Reference reference;
            System.out.println(str1);
            try {
                while ((reference = queue.poll()) != null) {
                    System.out.println(reference);
                }
            }catch (Exception e){}
        }
    }
    

    运行结果如下,字符串1倍强引用维持显然不可能被回收,之后从引用队列中取出了一个弱引用和一个虚引用,显然如上面提到的,弱引用和虚引用不影响对象的生命周期

    [GC (System.gc())  5243K->976K(251392K), 0.0012138 secs]
    [Full GC (System.gc())  976K->763K(251392K), 0.0051999 secs]
    hello
    java.lang.ref.WeakReference@4554617c
    java.lang.ref.PhantomReference@74a14482
    

    3、finalize方法

    事实上被判为不可达的对象并不是非死不可的,想要完全判定一个对象死亡,通常至少要经过两个阶段。若一个对象被发现与GC roots之间不存在引用链,则会被第一次标记为不可达并且进行一次筛选,检查该对象是否有必要执行finalize方法。当该对象没有覆盖finalize方法或者其finalize方法已被执行过,则都将被虚拟机视为没有必要执行的情况。

    若对象被判定为有必要执行finalize方法,则该对象会被放入一个名为F-Queue的队列中,并在稍后由一条虚拟机自动建立的低优先级线程Finalizer去触发队列中的对象的finalize方法。要注意的是Finalizer线程仅仅会去触发对象的finalize方法,而不会保证finalize方法的执行。

    finalize方法时Object类的成员方法,因此所有对象都拥有finalize方法,但finalize方法是一个空方法,若我们希望它被虚拟机调用,则需要覆盖它。

    finalize方法可以看做对象逃脱死亡命运的最后一次机会,也可以用来在对象死亡之前进行资源的释放与清理。在第一次标记过后GC会对F-Queue中的对象进行第二次的小规模标记,若对象在finalize方法中与任何引用链上的对象建立了关联,那么它就会暂时被移除需要回收的对象集合中,否则该对象就会被回收。此外要注意的是任何一个对象的finalize方法在其生命周期中只可能被调用一次,若一个对象第二次面临被回收,则其finalize方法不会执行,该对象也会被回收。我们来看以下GCTest类,我们覆写了finalize方法,在finalize方法中将对象与静态属性建立引用

    public class GCTest extends Object {
    
        public static GCTest hook = null;
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("执行finalize方法");
            hook = this;
        }
    
        public void isAlive(){
            System.out.println("对象存活");
        }
    
        public static void main(String[] args) throws InterruptedException {
            GCTest gcTest = new GCTest();
            gcTest = null;
            System.gc();
            Thread.sleep(500);
            if (hook != null){
                hook.isAlive();
            }else{
                System.out.println("对象已被回收");
            }
            hook = null;
            System.gc();
            if (hook != null){
                hook.isAlive();
            }else{
                System.out.println("对象已被回收");
            }
        }
    }
    

    执行结果如下,第一次回收后,对象被hook关联,而第二次回收后则没有

    [GC (System.gc())  1980K->856K(19968K), 0.0013165 secs]
    [Full GC (System.gc())  856K->757K(19968K), 0.0049851 secs]
    执行finalize方法
    对象存活
    [GC (System.gc())  1233K->949K(19968K), 0.0009255 secs]
    [Full GC (System.gc())  949K->789K(19968K), 0.0074109 secs]
    对象已被回收
    

    最后要注意的是,finalize方法的运行代价较高且不确定性大,虽然可以用来拯救对象或者关闭资源,但它能做到的事情,通常使用try-finally语句都能实现,因此并不推荐使用finalize方法

    四、分代收集理论——对象的分配与回收

    1、理论前提——几个假说

    • 弱分代假说:绝大多数对象都是朝生夕灭的
    • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消灭

    这两个分代假说理论奠定了常用垃圾收集器一致的设计原则:收集器应该将Java堆划分为不同的区域,然后将回收的对象依据其年龄分配到不同的区域之中存储。

    这样做的好处在于提高效率:若一个区域中的对象大多都是朝生夕灭的,那么每次垃圾收集过程中只需要关注如何保留少量存活的对象而不是去标记那些大量将要被回收的对象,可以以较低的代价回收到大量的空间;若剩下的都是难以消亡的对象,那么把他们集中放在一起,虚拟机便能使用较低的频率来回收这个区域。

    根据上面的理论,Java堆通常至少被划分为新生代(Young Generation)和老年代(Old Generation)两个部分。其中新生代中每次发生垃圾收集行为都会有大批对象死去,每次回收存活的少量的对象则会逐步晋升到老年代

    • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

    跨代引用假说事实上是由前两条假说推理得出的隐含推论,其表明互相引用关系的两个对象,是应该倾向于同时生存或同时消亡的,因此就没有必要在为了少量的跨代引用扫描整个老年代,也不需要浪费空间专门记录每一个对象是否存在跨代引用,只需要在新生代建立一个全局的数据结构——记忆集(Remembered Set),用来将老年代划分为若干个部分,标识出那一块可能存在跨代引用,这样每次收集新生代时只需要将存在跨代引用的块中的老年代对象加入到GC Roots进行扫描。但这种方法的缺点在于在对象引用关系发生改变时需要维护记录数据的正确性

    2、新生代与老年代

    • 新生代(Young Generation):用来存放新生对象,通常占据堆内存的1/3空间,垃圾收集行为频繁。新生代的垃圾回收采用标记复制算法,新生代被分为一个Eden区域和两个Survivor区域(这两个区域被命名为from和to),他们的大小通常为8:1:1。
    • 老年代(Old Generation/Tenured Space):老年代占用堆内存的2/3,存放生命周期较长的对象,垃圾回收相位不频繁,老年代一般使用标记整理算法进行回收

    3、垃圾收集行为的分类

    • 新生代收集(Minor GC/Young GC):目标只是新生代的收集
    • 老年代收集(Major GC/Old GC):目标是老年代的垃圾收集(除了CMS收集器外其他收集器不存在但对收集老年代的行为)
    • 混合收集(Mixed GC):目标是收集整个新生代以及部分老年代的垃圾收集(目前只有G1收集器存在这种行为)
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集

    4、从垃圾收集的角度看对象的生命周期

    a、对象的创建
    • 对象优先在Eden区分配

    大多数情况下,对象在新生代Eden区进行分配,当Eden区没有足够的空间进行分配时,虚拟机将会发起一次Minor GC,若Minor GC后,Eden空间仍然不够,那么对象就会被放入Survivor区域

    • 大对象直接进入老年代

    大对象指的是需要大量在连续内存空间的Java对象,最典型的大对象即长度很长的字符串,或者元素数量庞大的数组。在写程序时应当尽量避免创建大对象,大对象在分配空间时容易导致内存明明还有较多空间时就提前触发垃圾收集以获取足够的连续空间存放他们,此外在复制时大对象也会导致高额的内存开销

    例如下面这段代码,先对虚拟机参数进行设置,-Xms20m -Xmx20m -Xmn10m将Java堆容量设置为20m,禁止自动扩容,且新生代容量为10m,剩余的10m分配给老生代;-XX:SurvivorRatio=8将Eden与Survivor的内存比例设置为8:1;-XX:+PrintGCDetails设置虚拟机打印GC细节。

    /*
    	虚拟机参数:
    	-Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails
    */
    public class Test {
        public static void main(String[] args) {
            byte[] bytes = new byte[6*1024*1024];
        }
    }
    

    我们尝试创建一个大小为6m的数组,可以从执行结果看到,新生代占用了百分之三十,而老年代占用了百分之六十,显然数组对象被分配到了老年代

    Heap
     PSYoungGen      total 9216K, used 2487K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      eden space 8192K, 30% used [0x00000000ff600000,0x00000000ff86df90,0x00000000ffe00000)
      from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
      to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
     ParOldGen       total 10240K, used 6144K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      object space 10240K, 60% used [0x00000000fec00000,0x00000000ff200010,0x00000000ff600000)
     Metaspace       used 3229K, capacity 4496K, committed 4864K, reserved 1056768K
      class space    used 352K, capacity 388K, committed 512K, reserved 1048576K
    
    b、Minor GC时发生了什么

    Minor GC是针对新生代的垃圾收集行为,上面介绍过新生代采用的是标记复制算法,事实上两个Survivor区就是为标记——复制算法准备的。当垃圾收集发生时,垃圾收集器会扫描Eden和Survivor From Space区域,若发现对象存活,则将该对象复制到Survivor ToSpace区域中,若Survivor ToSpace区域没有足够的空间则直接复制到老年代。扫描结束后垃圾收集器将清空Eden和Survivor FormSpace并交换Survivor FromSpace和Survivor ToSpace的角色。因此新生代同一时刻只有两个区域在使用,新生代的实际可用内存空间也为新生代总内存空间的90%

    c、长期存活对象的晋升

    每一个对象都拥有一个年龄计数器,存储在对象头中,对象通常在Eden区诞生,若经过第一次Minor GC后该对象仍然存活,且能被Survivor容纳,则该对象会被移动到Survivor中,并且其年龄会被设为1。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当对象年龄达到阈值(默认为15),就会被晋升到老年代。对象年龄阈值可以使用-XX:MaxTenuringThreshold设置

    事实上,HotSpot虚拟机并不是始终要求对象的年龄达到一个固定的数值才能晋升老年代,若在Survivor中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代

    d、何时发生Full GC——空间分配担保

    如果我们需要去银行贷款,并且个人的信用很好,大部分情况下都能及时偿还,于是银行借给我们钱的同时,会需要我们有一个担保人保证我们能按时偿还,若不能按时偿还,则可以从担保人的账户进行扣款,以此降低风险。

    事实上在垃圾收集时也需要有这么一个担保。在进行Minor GC时若Survivor控件不足以容纳一次Minor GC之后存活的对象,就需要依赖老年代来进行分配担保。

    在Minor GC发生前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总和,若大于,则此次的Minor GC行为可以确保是安全的。否则,虚拟机会先查看-XX:HandlePromotionFailure参数设置的值是否允许担保失败,若允许,则会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则会尝试进行一次Minor GC,若小于,或-XX:HandlePromotionFailure设置不允许冒险,则进行一次Full GC。

    上面的尝试进行一次Minor GC,事实上是有风险的行为,使用老年代进行担保的前提是老年代还有足够的空间,但具体有多少对象存活,这在Minor GC发生之前都是未知的,只能通过以往的平均水平来做推断,决定是否需要进行Full GC,但如果遇到比较糟糕的情况,例如最极端的所有新生代对象都存活,那么只能重新发起一次Full GC,显然这会导致运行效率下降。虽然担保失败代价很大,但通常情况下还是会打开-XX:HandlePromotionFailure开关,避免频繁的Full GC

    在这里插入图片描述

    五、经典的垃圾收集器

    在这里插入图片描述

                                     HotSpot虚拟机中的垃圾收集器

    1、Serial收集器

    Serial收集器是最基础、历史最悠久的收集器,在jdk1.3之前是HotSpot虚拟机新生代唯一的收集器。Serial收集器是一个单线程的收集器,它最大的特点是“Stop the world”,即在进行垃圾收集时,必须暂停其他所有工作线程,直到它完成收集工作。它的却带也是显而易见的,由于垃圾收集行为完全是由虚拟机控制的,对于用户和开发者来说完全是不可知不可控的情况,因此对很多后台应用都会带来一定的麻烦。

    虽然如此,但Serial收集器依然是如今HotSpot虚拟机在客户端模式下的默认新生代收集器,其最大的优点在于简单高效,对于内存资源受限的环境,Serial收集器是所有收集器里额外内存消耗最小的

    2、ParNew收集器

    ParNew收集器实际上是Serial收集器的多线程并行版本,同样是一个Stop The World的收集器,除了同时使用多条线程进行垃圾收集之外,其余的行为,包括控制参数、收集算法、对象分配规则、回收策略等都与Serial收集器完全一致

    ParNew收集器与Serial相比除了多线程外并没有太多创新之处,但它被广泛运用在运行在服务器模式下的HotSpot虚拟机,其最主要的原因之一就是除了Serial之外,目前只有ParNew可以与CMS收集器配合工作

    3、Parallel Scavenge收集器

    Parallel Scavenge收集器是一个新生代收集器,其同样基于标记复制算法实现,支持多线程。Parallel Scavenge收集器与其他收集器相比,最大的特点在于,相较于缩短垃圾收集时的停顿时间,它更关注垃圾收集行为是否能达到一个可控制的吞吐量。吞吐量指的是处理器用于运行客户代码的时间与处理器总消耗时间的比值
    =+ 吞吐量=\frac{运行用户代码时间}{运行用户代码时间+运行垃圾收集的时间}
    高吞吐量意味着可以以高效率的利用处理器资源,尽快的完成运算任务,适合在后台运算而不需要太多交互的分析任务。

    • 吞吐量参数

    Parallel Scavenge收集器提供了两个用于精准控制吞吐量的参数,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMullis参数和直接设置吞吐量大小的-XX:GCTimeRatio参数

    -XX:MaxGCPauseMillis参数允许设置一个大于0 的毫秒数,收集器将尽力保证内存回收花费的时间不超过该值,但这并不意味这这个值设置的越小,垃圾回收行为就越快,垃圾收集停顿时间是以牺牲吞吐量和新生代空间为代价换取的,若这个最大值设置的太小了,单次垃圾回收行为也许会变快,但也会导致垃圾回收行为变得频繁,从而降低吞吐量

    -XX:GCTimeRatio参数应当是一个(0,100)内的整数,其表示垃圾收集时间占总时间的比率,若我们记该参数为n,那么最大垃圾收集时间占总时间的比率即为11+n\frac{1}{1+n}

    由于与吞吐量关系密切,Paralel Scavenge收集器还常常被称作“吞吐量优先收集器”。此外Parallel Scavenge收集器还有一个重要的参数-XX:+UseAdaptiveSizePolicy,显然这是一个和动态内存大小相关的参数,当该参数被打开后,就不再需要人工置顶新生代大小、Eden和Survivor的比例以及晋升老年代对象大小等参数,虚拟机会根据系统当前的运行情况收集性能监控信息,动态调节这些参数以提供最合适的提顿时间或最大吞吐量。这种垃圾收集行为成为自适应调节策略

    4、Serial Old收集器

    Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,采用标记整理算法,同样的主要也是为客户端模式下的虚拟机使用

    5、Parallel Old收集器

    Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用标记整理算法,支持多线程并发收集

    6、CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器,通常被广泛应用于互联网网站或基于浏览器的B/S系统的服务端,这类应用通常较为关注响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验

    • CMS的运行过程

    CMS收集器采用标记清除算法,其具体运行过程相较其他收集器较为复杂,包括:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)和并发清除(CMS concurrent sweep)四个步骤。其中初始标记与重新标记两个过程依然是Stop The World。初始标记会标记出于GC Roots直接关联的对象,速度很快;并发标记阶段会从GC Roots直接关联的对象开始遍历整个对象图,这个过程耗时较长,但不需要停顿用户线程;重新标记阶段则将修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;并发清楚阶段将清理删除掉标记阶段判断的已死的对象,由于仅仅进行清除而不移动可用对象,因此可与用户线程并发执行

    • 特点

    CMS是一款优秀的收集器,其最主要的优点在于并发收集与低停顿,因此有时也被称为并发低停顿收集器(Concurrent Low Pause Collector),但其也存在一些重要的缺点。

    CMS收集器由于其并发特性,对处理器资源较为敏感,在并发阶段,虽然不会导致用户线程的停顿,但也会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是+34\frac{处理器核心数量+3}{4},那么当处理器核心数量大于等于4时,CMS回收线程仅占用不超过25%的处理器资源,并且随着处理器核心数量的增加而降低,而当处理器核心数量小于4时,CMS就会对用户线程造成较大影响,本就紧张的处理器资源,在启动垃圾收集时要分出一半的运算能力给垃圾收集线程,导致用户线程执行速度突然大幅度降低。

    此外CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能会出现“Con-current Mode Failure”进而导致一次完全Stop The World的Full GC产生。浮动垃圾指在并发清理阶段由还在运行的用户线程产生的垃圾,这些垃圾在本次的GC中将无法清除,只能留到下一次GC。并且由于用户线程不会暂停,在垃圾收集行为期间就需要预留足够的内存空间提供给用户线程,因此CMS收集器并不会等到老年代几乎被填满了才进行收集,可以使用-XX:CMSInitiatingOccupancyFraction参数来控制CMS启动的阈值,在jdk1.5中,默认在老年代使用了68%的情况下激活CMS,而在jdk1.6时这个阈值被提高到了92%,但这又带来了并发失败的风险:CMS运行期间预留的内存无法满足程序分配新对象的需要,这时虚拟机将启动后备预案:动检用户线程,临时启用Serial Old收集器来重新进行老年代收集,从而导致更长的停顿时间。因此-XX:CMSInitiatingOccupancyFraction设置的太高会导致大量的并发失败,性能反而降低

    最后还有一个缺点,CMS收集器采用的是标记清楚算法,这意味着CMS收集器很容易产生大量内存碎片,当内存碎片过多时会对大对象的分配带来较大麻烦,往往会出现老年代仍有大量剩余空间但无法找到足够的连续空间来进行内存分配,进而提前触发Full GC。要解决这个问题可以使用-XX:+UseCMSCompactAtFullCollection参数(默认开启),该参数会使得CMS在不得不进行Full GC时开启内存碎片的整理过程,这样虽然可以解决碎片问题,但由于需要移动对象无法并发,导致停顿时间边长,因此也可以使用-XX:CMSFullGCsBeforeCompaction参数,该参数指明在CMS收集器在执行若干次不整理空间的Full GC后下一次FullGC开始前会进行素片整理,该参数默认值为0。但这两个参数在jdk9后均被废弃了

    7、G1收集器

    G1(Garbage First)收集器是一款主要面向服务端引用的垃圾收集器,HotSpot团队最初开发这款收集器的目的是替换CMS收集器,在jdk9之后G1取代了Parallel Scavenge+Parallel Old的组合,称为服务端模式下默认的垃圾收集器。G1收集器与之前介绍的所有收集器最大的不同在于G1是一个面向堆内存任何部分组成的回收集的收集器,其痕量标准不再是新生代与老年嗲,而是哪块内存中存放的垃圾街数量最多,回收收益最大。

    G1收集器的回收行为基于Region的堆内存布局进行,虽然它仍然遵循分代收集理论,但其堆内存布局与其他收集器有明显差异,G1收集器不再坚持固定大小及固定数量的分带区域划分,而是把连续的堆划分成多个等大的独立区域——Region,每个Region都可以根据需要扮演新生代、老年代空间。

    此外Region中还有一个特殊的Humongous区域用来专门存储大对象,只要大小超过了Region容量的一半即被认定为大对象。Region的大小可以使用-XX:G1HeapRegionSize参数进行设置,取值范围为1MB-32MB,且应为2的整数幂。对于超过整个Region容量的超大对象则被存放在N个连续的Humongous Region总。

    此外G1收集器可以建立可预测的停顿时间模型,因为它以Region作为单次回收的最小单元,即每次回收的内存怒空间都是Region大小的整数倍,这样可以有计划的避免在整个Java堆中进行全局的垃圾收集。G1会跟踪各个Region中的垃圾堆积的价值大小,并在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值最大的Region,这样的具有优先级的回收方式保证了G1在有限时间内尽可能更高效的进行收集

    • G1的运行过程

    G1收集器主要可以分为初始标记(Initial Marking)、并发标记(Concurrent Marking)、最终标记(Final Marking)和筛选回收(Live Data Counting Evacuation)四个步骤。其中初始标记、最终标记和筛选回收过程会暂停用户线程

    初始标记阶段仅仅会标记出GCRoots能直接关联到的对象,让下一阶段用户线程并发运行时能够正确的在可用的Region中分配新对象。

    并发标记阶段从GCRoots开始进行可达性分析,找出要回收的对象,耗时较长但可以与用户程序并发执行。在扫描完成后还需重新处理在并发时有引用变动的对象

    最终标记阶段会对用户线程做一个短暂的暂停,用于处理并发阶段遗留的有变动的对象。

    筛选回收阶段负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,并根据用户所期望的停顿时间来制定回收计划,可以自由的选择任意多个Region构成回收集,最后在把决定回收的Region中存活的对象复制到空的Region中,再清理掉旧Region中的全部空间

    六、常见的GC参数

    (并非所有)

    收集器的选择

    参数 描述
    -XX:UseSerialGC 虚拟机运行在Client模式下的默认值,开启后使用Serial+Serial Old的收集器组合进行内存回收
    -XX:UserParNewGC 开启后使用ParNew+Serial Old的收集器组合进行内存回收,在jdk9后被移除
    -XX:UseCon从MarkSweepGC 开启后使用ParNew+CMS+Serial Old的收集器组合进行内存回收,其中Serial Old将作为CMS出现并发失败时的后备策略
    -XX:UseParallelGC jdk9之前虚拟机运行在Server模式下的默认值,开启后使用Parallel Scavenge+Serial Old的收集器组合进行内存回收
    -XX:UseParallelOldGC       开启后使用Parallel Scavenge+Parallel Old的收集器组合
    -XX:UseG1GC 使用G1收集器,jdk9后Server模式的默认值

    新生代与老年代的调整

    参数 描述
    -XX:SurvivorRatio 设置新生代中Eden与Survivor空间的容量比值,默认大小为8:1
    -XX:PretenureSizeThreshold 设置直接晋升到老年代的对象的大小,超过该大小的对象将直接在老年代分配
    -XX:MaxTenuringThreshold 设置晋升到老年代的对象年龄,默认为15
    -XX:UseAdaptiveSizePolicy 动态调整堆中各个区域带下以及进入老年代的年龄
    -XX:HandlePromotionFailure 是否允许分配担保失败

    收集器性能相关

    参数 描述
    GCTimeRatio GC时间栈总时间的比率,默认为99,即允许1%的GC时间,仅在使用Parallel Scavenge收集器时生效
    -XX:MaxGCPauseMillis 设置GC最大停顿时间,仅在使用Parallel Scavenge时生效
    -XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代被使用多少后触发垃圾收集,默认值为68%,仅再使用CMS时生效
    -XX:UseCMSCompactAtFullCollection 设置CMS在完成垃圾收集后是否需要进行一次内存碎片清理,仅再使用CMS时生效,该参数在jdk9后废弃
    -XX:CMSFullGCsBeforeCompaction 设置CMS在进行若干次垃圾收集后启动一次内存碎片整理,仅在使用CMS时生效,该参数在jdk9后废弃
    -XX:G1HeapRegionSize=n 设置Region大小,并非最终值
    -XX:MaxGCPauseMillis 设置G1收集器目标时间,默认值为200ms
    展开全文
  • Java并发编程实践 大话设计模式 深入了解jvm虚拟机 算法导论
  • 深入了解JVM的底层原理 引言:什么是JVM? JVM在整个jdk(java 运行环境)中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java...

    深入了解JVM的底层原理

    引言:什么是JVM?
    JVM在整个jdk(java 运行环境)中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机. 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境.
    1.创建JVM装载环境和配置
    2.装载JVM.dll
    3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例
    4.调用JNIEnv实例装载并处理class类。

    一:JVM内存区域模型
    这里写图片描述
    1、方法区:
    也称“永久代”,“非堆”,用于储存虚拟机加载的类信息,常量,静态变量,是各个线程共享的内存区域。

    运行时常量池:方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。

    2、虚拟机栈:
    描述的是java方法执行的内存模型,每个方法被执行的时候,都会创建一个“栈帧”用于存储局部变量(包括参数),操作栈,方法出口等信息。每个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。生命周期与线程相同,是线程私有的。

    **局部变量表:**存放八种基本类型,对象引用,其中64位长度的long和double类型的数据会占用两个局部变量的空间,其余数据类型只占一个。局部变量表是在编译时完成分配的,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间不再改变。

    3、本地方法栈:
    与虚拟机栈基本类似,为Native方法(本地方法)服务。

    4、堆:
    也叫java堆,GC堆。是JVM中所管理的内存中最大的一块内存区域,是线程共享的,在JVM启动时创建。存放了对象的实例及数组(所有new的对象)。
    这里写图片描述
    Permanent Generation(方法区)主要用来放JVM自己的反射对象,比如类对象和方法对象等。
    Heap = {Old + NEW = { Eden , from, to } }
    JVM内存模型中分两大块,一块是 NEW Generation(新生代), 另一块是Old Generation(老年代). 在New Generation中,有一个叫Eden(伊甸园)的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from,to), 它们用来存放每次垃圾回收后存活下来的对象。在Old Generation中,主要存放应用程序中生命周期长的内存对象

    5、程序计数器
    是最小的一块内存,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,异常处理,线程恢复等基础功能都需要依赖计数器完成。

    6、直接内存:
    直接内存并不是虚拟机内存中的一部分,也不是JVM规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,即本机内存,不会影响到对内存的大小。

    二、垃圾收集机制
    这里写图片描述
    1、垃圾回收算法
    1.1、Mark-Sweep(标记-清除)算法
    是最基础的垃圾回收算法,其他算法都是基于这种思想。标记-清除算法分为“标记”,“清除”两个阶段:首先标记出需要回收的对象,标记完成后统一清除对象。
    缺点:1:标记和清除的效率不高。
    2:标记之后会产生大量不连续的内存碎片。
    1.2、Copying(复制)算法
    将可用内存分为两块,每次只用其中的一块,当这块内存用完以后,将还存活的对象复制到另一块上面,然后再把已经使用的内存空间一次清理掉。
    优点:1:不会产生内存碎片。
    2:只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。
    缺点:内存缩小为原来的一半
    1.3、Mark-Compact(标记-整理)算法
    标记操作和”标记-清除“算法一样,后续操作变成不直接清理对象,而是在清理无用对象的时候完成让所有存活的对象都像一端移动,并更新对象的指针。
    优点:不会产生内存碎片
    缺点:在“标记-清除”基础上还要进行对象的移动,成本相对较高
    1.4、Generational Collection(分代收集)算法(重点)
    是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
    目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
    而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
    注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
    2、垃圾收集器
    垃圾收集算法是 内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。
    2.1、Parallel Scavenge
    Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,它主要是为了达到一个可控的吞吐量。
    2.2、Parallel Old
    Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
    2.3、CMS
    CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。
    2.4、G1
    G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
    3、判断是否回收的算法
    1.引用计数器(jvm未使用)
    2,可达性分析(根节点搜索)
    这里写图片描述
    三、对象内存的分配与回收
    1、分配
    1.1、大部分对象在分配时都是在Eden中
    1.2、较大的对象直接分配到Old Generation中
    2、回收(GC)
    2.1、新生代GC(Minor GC):
    发生在新生代的垃圾回收动作,因为大多数对象都是朝生暮死的,所以Minor GC非常频繁,回收速度也比较快。
    2.2、老年代GC(Major GC/Full GC)
    发生在老年代的GC,发生Full GC时,一般会伴随着一次Minor GC,Full GC的速度比较一般会比Minor GC慢10倍以上。

    四、对象的访问和对象引用强度
    1、对象的访问
    对象的访问涉及到java栈,java堆,方法区三个内存区域。
    1.1、句柄访问方式:
    java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄,而句柄中包含了对象的实例数据(实例)和类型数据(class信息)各自的具体地址信息。
    这里写图片描述
    1.2、指针访问方式
    reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据的地址。
    这里写图片描述
    1.3、两种方式的对比
    使用句柄访问方式最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。
    使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式(直接指针访问)
    2、对象的引用强度
    2.1、强引用:
    就是指在代码之中普遍存在的,类似:“Object objectRef = new Obejct”,这种 引用,只要强引用还存在,永远不会被GC清理。
    2.2、软引用:
    用来描述一些还有用,但并非必须存在的对象,当Jvm内存不足时(内存溢出之前) 会被回收,如果执行GC后,还是没有足够的空间,才会抛出内存溢出异常。
    通过SoftReference类来实现软引用,SoftReference很适合用于实现缓存。另,当GC认为扫描的SoftReference不经常使用时,可会进行回收。可用softReference.get()获取
    2.3、弱引用
    弱引用也是用来描述一些还有用,但并非必须存在的对象,它的强度会被软引用弱 些,被弱引用关联的对象,只能生存到下一次GC前,当GC工作时,无论内存是否足够, 都会回收掉弱引用关联的对象。JDK通过WeakReference类来实现。
    当获取时,可通过weakReference.get方法获取,可能返回null。
    可传入一个ReferenceQueue对象到WeakReference构造,当引用对象被表示为可回收 时,isEnqueued返回true
    2.4、虚引用
    虚引用称为“幻影引用”,它是最弱的一种引用关系,一个对象是否有虚引用的存 在,完全不会对生存时间构成影响。为一个对象设置虚引用关联的唯一目的就是希望能 在这个对象被GC回收时收到一个系统通知。
    可以通过PhantomReference类实现。值得注意的是:phantomReference.get方法永远返回null, 当user从内存中删除时,调用isEnqueued会返回true
    五、内存调优****重点内容
    原因:过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量
    目的:减少Full GC次数,减少GC频率,尽量降低CG所导致的应用线程暂停时间。
    手段:主要是针对内存管理方面的调优,包括控制各个代的大小,GC策略。
    内存控制:
    1、导致Full GC的原因
    1.1、旧生代空间不足
    调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要 创建过大的对象及数组避免直接在旧生代创建对象
    1.2、Pemanet Generation空间不足
    增大Perm Gen空间,避免太多静态对象 。
    1.3、统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间
    控制好新生代和旧生代的比例
    1.4、System.gc()被显示调用
    垃圾回收不要手动触发,尽量依靠JVM自身的机制
    2、控制堆内存的各个部分的比例和GC策略失调
    2.1、新生代设置过小
    一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代, 占据了旧生代剩余空间,诱发Full GC
    2.2、新生代设置过大
    一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是 新生代GC耗时大幅度增加。一般说来新生代占整个堆1/3比较合适
    2.3、Survivor设置过小Survivor设置过小
    导致对象从eden直接到达旧生代,降低了在新生代的存活时间
    2.4、Survivor设置过大
    导致eden过小,增加了GC频率
    2.5、新生代存活时间太少
    通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收
    GC策略
    1、吞吐量优先
    JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置
    2、暂停时间优先
    JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置
    JVM常见配置

    1、堆设置
    -Xms:初始堆大小
    -Xmx:最大堆大小
    -XX:NewSize=n:设置年轻代大小
    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:	3,年轻代占整个年轻代年老代和的1/4
    -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区	有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
    -XX:MaxPermSize=n:设置持久代大小
    
    2、收集器设置
    -XX:+UseSerialGC:设置串行收集器
    -XX:+UseParallelGC:设置并行收集器
    -XX:+UseParalledlOldGC:设置并行年老代收集器
    
    3、垃圾回收统计信息
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -Xloggc:filename
    
    4、并行收集器设置
    -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    
    5、并发收集器设置
    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU	数。并行收集线程数。
    注意:在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    六、JDK可视化监控工具
    JConsole 在bin目录下,自动后自动搜索jvm进程,不需要指定。
    有概述,内存,线程,类,VM摘要和Mbean六个页签。
    概述:显示有关JVM的监测值
    .
    内存: 显示内存使用信息

    注意垃圾回收次数、时间、以及partial GC和full GC

    线程: 显示线程使用信息

    类: 显示类装载信息

    VM摘要:显示java VM信息

    MBeans: 显示 MBeans.

    七、类加载机制
    1.类加载的过程
    这里写图片描述
    1.1、加载:查找并加载类的二进制数据
    1.2、验证:确保被加载的类的正确性
    1.3、准备:为类的静态变量分配内存,并将其初始化为默认值
    1.4、解析:把类中的符号引用转换为直接引用
    1.5、初始化,为类的静态变量赋予正确的初始值
    1.6、使用:主动使用和被动使用
    1.7、卸载:满足条件后,被GC回收
    2、ClassLoader的加载原理
    ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),当父亲已经加载了该类的时候,子ClassLoader不再加载,避免了重复加载。
    双亲委派模型的工作工程:当一个类加载器收到一个类加载请求时,它首先不会先去加载这个类,而是把这个请求委托给父加载器,每一层的加载器都是如此,所以最终所有的加载请求都会传送到最上层的启动加载器。只有当父加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类),子加载器才会尝试自己去加载。
    这里写图片描述

    最后补充一句很重要的知识点:当你的jvm出现问题时,你怎么定位到错误的起源:
    通过jstack可以找到,具体怎么实现的,网上有很多,我就不说了。

    展开全文
  • 深入了解JVM-方法区

    2020-04-20 08:51:58
    深入了解JVM-方法区 当JVM使用类装载器装载某个类时,它首先要定位对应的class文件,然后读入这个class文件,最后,JVM提取该文件的内容信息,并将这些信息存储到方法区,最后返回一个class实例。上面是对类的装载...
  • 深入了解JVM(GC篇)

    2020-05-07 14:26:23
    深入了解JVM(其一)JAVA的垃圾回收机制JVM的内存模型何时进行垃圾回收引用计数法可达性分析法四种引用被回收之前方法区的回收垃圾回收算法分代收集理论4种垃圾回收算法HotSpot算法细节根节点枚举安全点安全区域记忆...
  • 深入了解JVM之垃圾回收器。 1.Serial收集器 Serial收集器是java最古老的收集器之一,特点就是单线程,此处单线程指的是,在GC旗舰,必须暂停其他所有工作线程,也就是所谓的STW(Stop The World),所以此回收器的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,537
精华内容 1,414
关键字:

深入了解jvm