Java基础知识(六)

foreach与正常for循环效率对比

在遍历数组时,foreach的表现要稍微好一点,在遍历集合的时候,for的表现要好一点。但是不管哪种情况,for和foreach这两种遍历方式时间都相差不大。因此对于这两者的比较在时间效率来说应该相差不会很大。主要是在对于两者的应用场景上的选择:
(1)普通for循环可以根据下标来访问;
(2)foreach在代码结构上更加清晰、简单;
(3)foreach在遍历的时候会锁定集合中的对象,期间不能修改,而for能对集合中的元素进行修改。

Java IO与NIO

  • 什么是IO:

    1. File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
    2. InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
    3. OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。
    4. Reader(文件格式操作):抽象类,基于字符的输入操作。
    5. Writer(文件格式操作):抽象类,基于字符的输出操作。
    6. RandomAccessFile(随机文件操作):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
  • NIO(New IO):
    Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
    Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
    Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

  • 区别:
    1:面向流与面向缓冲
    Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 JavaNIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
    2:阻塞与非阻塞IO
    Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 JavaNIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

Java与C++对比

  • 指针 Java 没有指针的概念,从而有效地防止了在 C/C++语言中,容易出现的指针操作失误(如指针悬空所造成的系统崩溃)。在 C/C++中,指针操作内存时,经常会出现错误。在Java 中没有指针,更有利于 Java 程序的安全。

  • 多重继承 C++支持多重继承,它允许多父类派生一个子类。也就是说,一个类允许继承多个父类。尽管多重继承功能很强,但使用复杂,而且会引起许多麻烦,编译程序实现它也很不容易。所以 Java 不支持多重继承,但允许一个类实现多个接口。可见,Java 既实现了 C++多重继承的功能,又避免了 C++的许多缺陷。

  • 数据类型 Java 是完全面向对象的语言,所有方法和数据都必须是类的一部分。除了基本数据类型之外,其余类型的数据都作为对象型数据。例如对象型数据包括字符串和数组。类将数据和方法结合起来,把它们封装在其中,这样每个对象都可实现具有自己特点的行为。而 C++将函数和变量定义为全局的,然后再来调用这些函数和变量,从而增加了程序的负担。此外,Java 还取消了 C/C++中的结构和联合,使编译程序更简洁。

  • 自动内存管理 Java 程序中所有的对象都是用 new 操作符建立在堆栈上的,这个操作符类似于 C++的“new”操作符。Java 自动进行无用内存回收操作,不需要程序员进行删除。当 Java 中一个对象不再被用到时,无须使用内存回收器,只需要给它加上标签以示删除。无用内存的回收器在后台运行,利用空闲时间工作。而 C++中必须由程序释放内存资源,增加了程序设计者的负担。

  • 操作符重载 Java 不支持操作符重载,操作符重载被认为是 C++的突出特征。在 Java 中虽然类可以实现这样的功能,但不支持操作符重载,这样是为了保持 Java 语言尽可能简单。

  • 预处理功能 C/C++在编译过程中都有一个预编译阶段,即预处理器。预处理器为开发人员提供了方便,但增加了编译的复杂性。Java 允许预处理,但不支持预处理器功能,因为 Java 没有预处理器,所以为了实现预处理,它提供了引入语句(import),它与 C++预处理器的功能类似。

  • Java 不支持缺省函数参数,而 C++支持。 在 C 中,代码组织在函数中,函数可以访问程序的全局变量。C++增加了类,提供了类算法,该算法是与类相连的函数,C++类方法与 Java 类方法十分相似。由于 C++仍然支持 C,所以 C++程序中仍然可以使用 C 的函数,结果导致函数和方法混合使用,使得 C++程序比较混乱。

  • Java 没有函数,作为一个比 C++更纯的面向对象的语言。Java 强迫开发人员把所有例行程序包括在类中。事实上,用方法实现例行程序可激励开发人员更好地组织编码。

  • 字符串 C 和 C++不支持字符串变量,在 C 和 C++程序中使用“Null”终止符代表字符串的结束,在 Java 中字符串是用类对象(String 和 StringBuffer)来实现的,在整个系统中建立字符串和访问字符串元素的方法是一致的。Java 字符串类是作为 Java 语言的一部分定义的,而不是作为外加的延伸部分。此外,Java 还可以对字符串用“+”进行连接操作。

  • goto 语句 “可怕”的 goto 语句是 C 和 C++的“遗物”。它是该语言技术上的合法部分,引用 goto语句造成了程序结构的混乱,不易理解。goto 语句一般用于无条件转移子程序和多结构分支技术。Java 不提供 goto 语句,其虽然指定 goto 作为关键字,但不支持它的使用,这使程序更简洁易读。

  • 类型转换 在 C 和 C++中,有时出现数据类型的隐含转换,这就涉及了自动强制类型转换问题。例如,在 C++中可将一个浮点值赋予整型变量,并去掉其尾数。Java 不支持 C++中的自动强制类型转换,如果需要,必须由程序显式进行强制类型转换。

Java1.7与1.8新特性

  • 1.7新特性

    • switch支持string(前几篇笔记里提到过)
    • 创建泛型实例,可以通过类型推断简化代码,new后面的<>内不用再写泛型

      HashMap<String, String> parmas = new HashMap<>();
    • try-with-resource语句实现自动资源管理,在try执行完毕后自动关闭资源,关闭的资源需要实现java.lang.AutoCloseable接口

      private static void customBufferStreamCopy(File source, File target) {
        try (InputStream fis = new FileInputStream(source);
            OutputStream fos = new FileOutputStream(target)){
      
            byte[] buf = new byte[8192];
      
            int i;
            while ((i = fis.read(buf)) != -1) {
                fos.write(buf, 0, i);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
      }
    • 单个catch捕捉多个异常,异常之间用管道符(|)隔开

     public static void testThrows() throws IOException, SQLException { 
            try { 
                 testThrows(); 
             } catch (IOException | SQLException ex) { 
                 throw ex; 
             } 
         }
  • Java1.8新特性

    • lambda表达式,功能接口(只有一个方法的接口)
    • 接口允许添加非抽象方法,需要添加default字段

      public interface Demo {
            default public int add(int a,int b){
                return a+b;
            }
          }
    • 允许使用::关子健传递方法或者构造函数

JNI简介

JNI是Java平台中的一个非常强大的特性,通过JNI相关的接口,我们不仅可以在Java中调用本地代码,也可以在本地代码使用Java完成相关的操作,并且不需要考虑操作系统之间的差异。那么JNI是如何消除操作系统之间的差异的呢?首先,我们要理解两个概念:Java平台和主机环境。Java平台是包含了Java虚拟机和Java API接口的编程环境,Java应用程序会被编译成与机器无关的二进制格式的类文件,这些类文件能够被Java虚拟机解析执行。主机环境表示主机操作系统,本地库和计算机指令集。使用C/C++编写的本地代码会被编译成与机器相关的二进制代码并与本地库链接起来。JRE(Java Runtime Environment)就是这样的一个主机环境。
Java应用程序在Java虚拟机里解析执行,Java虚拟机在主机环境里运行,并通过JNI接口与本地应用程序或者代码库进行通信。
需要强调的一点是,一旦使用JNI去编写应用程序就意味着你将失去Java平台的两个特性:
1. Java应用程序不再跨平台运行。即便Java语言编写的部分是跨平台的,但是你仍然需要将本地代码部分重新编译成与平台相关的代码;
2. 本地代码并不是类型安全的语言,然而Java却是。因此,如果本地代码稍有差错都回导致Java虚拟机崩溃。基于这一点,在执行JNI之前进行安全检查是非常重要的一个步骤。

JVM内存模型以及分区,每个区放什么

Java虚拟机在程序执行过程会把jvm的内存分为若干个不同的数据区域来管理,这些区域有自己的用途,以及创建和销毁时间。
jvm管理的内存区域包括以下几个区域:

  • 栈区:
    栈分为java虚拟机栈本地方法栈
    重点是Java虚拟机栈,它是线程私有的,生命周期与线程相同
    每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。
    通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型,及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。
    会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。
    本地方法栈 为虚拟机使用到本地方法服务(native)。

  • 堆区:
    堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例
    堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区最要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。
    不过很多文章介绍分为3个区块,把方法区算着为永久代。这大概是基于Hotspot虚拟机划分, 然后比如IBM j9就不存在永久代概论。不管怎么分区,都是存放对象实例。
    会有异常OutOfMemoneyError。

  • 方法区:
    被所有线程共享区域用于存放已被虚拟机加载的类信息,常量,静态变量等数据被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
    垃圾回收很少光顾这个区域,不过也是需要回收的,主要针对常量池回收,类型卸载。
    常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如string的intern()方法。

  • 程序计数器:
    当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
    Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的
    唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。

堆里面的分区:Eden,survival from to,老年代,各自的特点

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor
Java基础知识(六)_第1张图片
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。
老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

特点:

  • Eden区
    Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。
  • Survival from to
    Survival区与Eden区相同都在Java堆的年轻代。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survival from区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代
  • 年老代
    年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

对象创建方法,对象的内存分配,对象的访问定位

  • 对象的创建
    Java对象的创建大致上有以下几个步骤:

    • 类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程
    • 为对象分配内存:对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。由于堆被线程共享,因此此过程需要进行同步处理(分配在TLAB上不需要同步)
    • 内存空间初始化:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
    • 对象设置:JVM对对象头进行必要的设置,保存一些对象的信息(指明是哪个类的实例,哈希码,GC年龄等)
    • init:执行完上面的4个步骤后,对JVM来说对象已经创建完毕了,但对于Java程序来说,我们还需要对对象进行一些必要的初始化。
  • 对象的内存分配
    Java对象的内存分配有两种情况,由Java堆是否规整来决定Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定):

    • 指针碰撞(Bump the pointer):如果Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离
    • 空闲列表(Free List):如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
  • 对象的访问定位
    对象的访问形式取决于虚拟机的实现,目前主流的访问方式有使用句柄和直接指针两种:

    • 使用句柄
      如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息:
      优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
    • 直接指针
      如果使用直接指针访问对象,那么对象的实例数据中就包含一个指向对象类型数据的指针,引用中存的直接就是对象的地址:
      优势:速度更快,节省了一次指针定位的时间开销,积少成多的效应非常可观。

GC的两种判定方法:引用计数与引用链

基于引用计数与基于引用链这两大类别的自动内存管理方式最大的不同之处在于:前者只需要局部信息,而后者需要全局信息

  • 引用计数
    引用计数顾名思义,就是记录下一个对象被引用指向的次数。引用计数方式最基本的形态就是让每个被管理的对象与一个引用计数器关联在一起,该计数器记录着该对象当前被引用的次数,每当创建一个新的引用指向该对象时其计数器就加1,每当指向该对象的引用失效时计数器就减1。当该计数器的值降到0就认为对象死亡。每个计数器只记录了其对应对象的局部信息——被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象;但也因为缺乏全局对象图信息,所以无法处理循环引用的状况。

  • 引用链
    引用链需要内存的全局信息,当使用引用链进行GC时,从对象图的“根”(GC Root,必然是活的引用,包括栈中的引用,类静态属性的引用,常量的引用,JNI的引用等)出发扫描出去,基于引用的可到达性算法来判断对象的生死。这使得对象的生死状态能批量的被识别出来,然后批量释放死对象。引用链不需要显式维护对象的引用计数,只在GC使用可达性算法遍历全局信息的时候判断对象是否被引用,是否存活。

GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路

  • 标记清除

    • 标记清除算法分两步执行:
      • 暂停用户线程,通过GC Root使用可达性算法标记存活对象。
      • 清除未被标记的垃圾对象。
    • 标记清除算法缺点如下:
      • 效率较低,需要暂停用户线程
      • 清除垃圾对象后内存空间不连续,存在较多内存碎片
    • 标记算法如今使用的较少了。
  • 标记整理

    • 标记整理算法是标记清除算法的改进,分为标记、整理两步:

      • 暂停用户线程,标记所有存活对象。
      • 移动所有存活对象,按内存地址次序一次排列,回收末端对象以后的内存空间。
    • 标记整理算法与标记清除算法相比,整理出的内存是连续的;而与复制算法相比,不需要多片内存空间。
      然而标记整理算法的第二步整理过程较为麻烦,需要整理存活对象的引用地址,理论上来说效率要低于复制算法。

    • 因此标记整理算法一般引用于老年代的Major GC(full GC)
  • 复制算法

    • 复制算法也分两步执行,在复制算法中一般会有至少两片的内存空间(一片是活动空间,里面含有各种对象,另一片是空闲空间,里面是空的):

      • 暂停用户线程,标记活动空间的存活对象。
      • 把活动空间的存活对象复制到空闲空间去,清除活动空间。
    • 复制算法相比标记清除算法,优势在于其垃圾回收后的内存是连续的。
      但是复制算法的缺点也很明显:

      • 需要浪费一定的内存作为空闲空间。
      • 如果对象的存活率很高,则需要复制大量存活对象,导致效率低下。
    • 复制算法一般用于年轻代的Minor GC,主要是因为年轻代的大部分对象存活率都较低。

并发(Concurrent)与并行(Parallel)

当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。这种方式我们称之为并发(Concurrent)
当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)

GC收集器有哪些?CMS收集器与G1收集器的特点

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

Java基础知识(六)_第2张图片

图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

  • Serial收集器
    Serial收集器是最基本的收集器。

    • 特性:
      这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
    • 优势:
      简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
  • ParNew收集器

    • 特性:
      ParNew收集器其实就是Serial收集器的多线程版本。在实现上,这两种收集器也共用了相当多的代码。除了Serial收集器外,目前只有它能与CMS收集器配合工作CMS作为老年代的收集器,却无法与新生代收集器Parallel Scavenge配合工作

    • Serial收集器 VS ParNew收集器:
      ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
      然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

  • Parallel Scavenge收集器

    • 特性:
      Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

    • 应用场景:
      停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

    • 对比分析:
      Parallel Scavenge收集器 VS CMS等收集器:
      Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。

    • Parallel Scavenge收集器 VS ParNew收集器:
      Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

    • GC自适应的调节策略:
      Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

  • Serial Old收集器

    • 特性:
      Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
  • Parallel Old收集器

    • 特性:
      Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
  • CMS收集器

    • 特性:
      CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

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

      • 初始标记(CMS initial mark)
        初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”(暂停其他所有的工作线程)。

      • 并发标记(CMS concurrent mark)
        并发标记阶段就是进行GC Roots Tracing的过程。

      • 重新标记(CMS remark)
        重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。

      • 并发清除(CMS concurrent sweep)
        并发清除阶段会清除对象。

    • 优点:
      由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
      CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。

    • 缺点:
      CMS收集器对CPU资源非常敏感

      • 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
        CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
      • CMS收集器无法处理浮动垃圾
        CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
        由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
        也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

      • CMS收集器会产生大量空间碎片
        CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生
        空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

  • G1收集器

    • 特性:
      G1(Garbage-First)是一款面向服务端应用的垃圾收集器。

      • 并行与并发
        G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

      • 分代收集
        与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

      • 空间整合
        与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

      • 可预测的停顿
        这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

      在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合

      G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

    • 执行过程:
      G1收集器的运作大致可划分为以下几个步骤:

      • 初始标记(Initial Marking)
        初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

      • 并发标记(Concurrent Marking)
        并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

      • 最终标记(Final Marking)
        最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

      • 筛选回收(Live Data Counting and Evacuation)
        筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

Minor GC与Full GC分别在什么时候发生?

Minor GC也叫Young GC,当年轻代内存满的时候会触发,会对年轻代进行GC。
Full GC也叫Major GC,当年老代满的时候会触发,当我们调用System.gc时也可能会触发,会对年轻代和年老代进行GC。

几种常用的内存调试工具:jmap、jstack、jconsole

  • jmap(linux下特有,也是很常用的一个命令)观察运行中的jvm物理内存的占用情况。 参数如下: -heap:打印jvm heap的情况 -histo:打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。 -histo:live :同上,但是只答应存活对象的情况 -permstat:打印permanent generation heap情况
  • jstack(linux下特有)可以观察到jvm中当前所有线程的运行情况和线程当前状态
  • jconsole一个图形化界面,可以观察到java进程的gc,class,内存等信息 jstat最后要重点介绍下这个命令。这是jdk命令中比较重要,也是相当实用的一个命令,可以观察到classloader,compiler,gc相关信息 具体参数如下: -class:统计class loader行为信息 -compile:统计编译行为信息 -gc:统计jdk gc时heap信息 -gccapacity:统计不同的generations(不知道怎么翻译好,包括新生区,老年区,permanent区)相应的heap容量情况 -gccause:统计gc的情况,(同-gcutil)和引起gc的事件 -gcnew:统计gc时,新生代的情况 -gcnewcapacity:统计gc时,新生代heap容量 -gcold:统计gc时,老年区的情况 -gcoldcapacity:统计gc时,老年区heap容量 -gcpermcapacity:统计gc时,permanent区heap容量 -gcutil:统计gc时,heap情况。

类加载的五个过程:加载、验证、准备、解析、初始化

JVM把class文件加载的内存,并对数据进行校验、转换解析和初始化,最终形成JVM可以直接使用的Java类型的过程就是加载机制。

  • 加载
    在加载阶段,虚拟机需要完成以下事情:
    • 通过一个类的权限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  • 验证
    在验证阶段,虚拟机主要完成:
    • 文件格式验证:验证class文件格式规范
    • 元数据验证:这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求
    • 字节码验证:进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
    • 符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问
  • 准备
    准备阶段是正式为类变量(被static修饰的变量)分配内存并设置变量初始值(0值)的阶段,这些内存都将在方法区中进行分配
  • 解析
    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
    常见的解析有四种:
    • 类或接口的解析
    • 字段解析
    • 类方法解析
    • 接口方法解析
  • 初始化
    初始化阶段才真正开始执行类中定义的java程序代码,初始化阶段是执行类构造器()方法的过程

双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader

  • 启动类加载器,负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即时放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用
  • 扩展类加载器:负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用该类加载器
  • 应用程序类加载器:负责加载用户路径上所指定的类库,开发者可以直接使用这个类加载器,也是默认的类加载器。 三种加载器的关系:启动类加载器->扩展类加载器->应用程序类加载器->自定义类加载器。
    这种关系即为类加载器的双亲委派模型。其要求除启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不以继承关系实现,而是用组合的方式来复用父类的代码。

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

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

实现:在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

分派:静态分派与动态分派

要理解分派,我们先来理解Java中的两种类型:
静态类型:变量声明时的类型。
实际类型:变量实例化时采用的类型。

举例的Java代码如下:
class Car {}
class Bus extends Car {}
public class Main {
public static void main(String[] args) throws Exception {
// Car 为静态类型,Bus 为实际类型
Car car = new Bus();
}
}

静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(重载是通过参数的静态类型而不是实际类型来选择重载的版本的)。
举例Java代码如下:
class Car {}
class Bus extends Car {}
class Jeep extends Car {}
public class Main {
public static void main(String[] args) throws Exception {
// Car 为静态类型,Car 为实际类型
Car car1 = new Car();
// Car 为静态类型,Bus 为实际类型
Car car2 = new Bus();
// Car 为静态类型,Jeep 为实际类型
Car car3 = new Jeep();

    showCar(car1);
    showCar(car2);
    showCar(car3);
}
private static void showCar(Car car) {
    System.out.println("I have a Car !");
}
private static void showCar(Bus bus) {
    System.out.println("I have a Bus !");
}
private static void showCar(Jeep jeep) {
    System.out.println("I have a Jeep !");
}

}
代码输出如下:
这里写图片描述
静态分派重载
从上面的例子我们可以看出重载调用的具体方法版本是由静态类型来决定的。

动态分派
与静态分派类似,动态分派指在在运行期根据实际类型确定方法执行版本,其典型应用是方法重写(即多态)。
举例Java代码如下:
class Car {
public void showCar() {
System.out.println(“I have a Car !”);
}
}
class Bus extends Car {
public void showCar() {
System.out.println(“I have a Bus !”);
}
}
class Jeep extends Car {
public void showCar() {
System.out.println(“I have a Jeep !”);
}
}
public class Main {
public static void main(String[] args) throws Exception {
// Car 为静态类型,Car 为实际类型
Car car1 = new Car();
// Car 为静态类型,Bus 为实际类型
Car car2 = new Bus();
// Car 为静态类型,Jeep 为实际类型
Car car3 = new Jeep();

    car1.showCar();
    car2.showCar();
    car3.showCar();
}

}
运行结果如下:
这里写图片描述
动态分派重写
可以看出来重写是一个根据实际类型决定方法版本的动态分派过程。

感谢各位前辈的分享,我在文末留下了相应的链接,在这里只是做了一个总结,作为学习笔记。希望能有所帮助。

面试心得与总结—BAT、网易、蘑菇街
foreach与正常for循环效率对比
关于java NIO和IO的区别介绍
Java与C++对比
Java1.7和1.8新特性
Java Native Interface(JNI) 简介
JVM内存模型及分区
Java 堆内存
JAVA面试题之JVM篇
关于JVM的常见问题(一)
关于JVM的常见问题(二)
深入理解JVM(5) : Java垃圾收集器
并发(Concurrent)与并行(Parallel)
LearningNotes

你可能感兴趣的:(Java)