美文网首页
Java 核心技术 - Java编程语言概述(二)

Java 核心技术 - Java编程语言概述(二)

作者: sumeng夙梦 | 来源:发表于2019-09-29 18:29 被阅读0次
1.1.4 Java核心机制与JVM运行原理

Java虚拟机(Java Virtual Machine)

(1)JVM 的基础概念

JVM 是由软件技术模拟出计算机运行的一个虚拟的计算机。

JVM 也充当着一个翻译官的角色,我们编写出的Java程序,是不能够被操作系统所直接识别的,这时候JVM的作用就体现出来了,它负责把我们的程序翻译给系统“听”,告诉它我们的程序需要做什么操作。

我们都知道Java的程序需要经过编译后,产生.class 文件,JVM才能识别运行并运行它,JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能运行起来,这就是Java能一次编译,到处运行的原因。

(2)JVM的生命周期

JVM在Java程序开始执行的时候,它才运行,程序结束的时,它就停止。

一个Java程序会开启一个JVM进程,如果一台机器上运行三个程序,那么就会有三个运行中的JVM进程。

JVM中的线程分为两种:守护线程和普通线程

守护线程是JVM自己使用的线程,比如垃圾回收(GC)就是一个守护线程。

普通线程一般是Java程序的线程,只要JVM中有普通线程在执行,那么JVM就不会停止。

权限足够的话,可以调用 exit() 方法终止程序。

(3)JVM的结构体系


JVM的结构体系.png

(4)JVM的启动过程

①JVM 的装入环境和配置

JDK:是面向开发人员使用的SDK,塔提供了Java的开发环境和运行环境,JDK中包含了JRE。

JRE:是Java的运行环境,是面向所有Java程序的使用者,包括开发者。

JRE = 运行环境 = JVM

java.exe 按照以下的顺序来选择JRE:

(1)自己目录下有没有JRE

(2)父目录下有没有JRE

(3)查询注册表

②装载JVM

通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。

③初始化JVM,获得本地调用接口

调用InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。

④运行Java程序

JVM运行Java程序的方式有两种:jarClass

运行jar的时候:Java.exe 调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getMainfest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。

运行Class的时候:main函数直接调用 java.c 中的LoadClass方法装载该类。

(5)Class 文件

Class文件由Java编译器生成,我们创建的.java文件在经过编译后,会变成 .class 的文件,这样才能被JVM所识别并运行。

(6)类加载子系统

类加载子系统也可以称之为类加载器,JVM默认提供三个类加载器:

BootStrap ClassLoader:称之为启动类加载器,最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jarresources.jarcharsets.jar等。

Extension ClassLoader:称之为扩展类加载器,复制加载Java的扩展类库,默认加载 $JAVA_HOMjre/lib/*.jar-Djava.ext.dirs 指定目录下的jar包。

App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jarclass文件。

除了Java默认提供的三个ClassLoader(加载器)之外,我们还可以根据自身定义ClassLoader,自定义ClassLoader必须继承java.lang.ClassLoader 类。

(7)方法区(Method Area)

在JVM中,类型信息类静态变量都保存在方法区中,类型信息是由类加载器在类加载的过程中从类文件中提取出来的信息。

需要注意的一点是。常量池也存放于方法区中

程序中所有的线程共享一个方法区,所以访问方法区的信息必须保证线程是安全的。如果有两个线程同时去加载一个类,那么只能有一个线程被允许加载这个类,另一个必须等待。

在程序允许时,方法区的大小是可以改变的,程序在运行时可以扩展。

方法区可以被垃圾回收,但条件非常严苛,必须在该类没有任何引用的情况下。

类型信息包括

  • 类型的全名(The fully qualified name of the type)

  • 类型的父类型全名(除非没有父类型,或者父类型是java.lang.Object)(The fully qualified name of the typeís direct superclass)

  • 该类型是一个类还是接口(class or an interface)(Whether or not the type is a class )

  • 类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)

  • 所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)

  • 类型的字段信息(Field information)

  • 类型的方法信息(Method information)

  • 所有静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)

  • 一个指向类加载器的引用(A reference to class ClassLoader)

  • 一个指向Class类的引用(A reference to class Class)

  • 基本类型的常量池(The constant pool for the type)

    方法列表

    为了更高效的访问所有保存在方法区中的数据,在方法区中,除了保存上边的这些类型信息之外,还有一个为了加快存取速度而设计的数据结构:方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例方法的引用,保存那些父类中调用的方法。

(8)Java堆(JVM堆、Heap)

当Java创建一个类的实例对象或者数组时,都在堆中为新的对象分配内存。

虚拟机只有一个堆,程序中所有的线程都共享它。

堆占用的内存空间是最多的。

堆的存取类型为管道类型,先进为出。

在程序运行中,可以动态的分配堆的内存大小。

(9)Java栈(JVM栈、Stack)

在Java栈中只保存基础数据类型和自定义对象的引用,注意只是对象的引用而不是对象本身哦,对象是保存在堆区中的。

像String、Integer、Byte、Short、Long、Character、Boolean这六个属于包装类型,它们是存放于堆中的。

栈的存取类型为类似于水杯,先进后出。

栈的数据在超出其作用域后,会被自动释放掉,它不由JVM GC 管理。

每一个线程都包括含一个栈区,每个栈中的数据都是私有的,其他栈不能访问。

每个线程都会建立一个操作栈,每个栈又包含了若干个栈帧,每个栈帧对应着每个方法啊的每次调用,每个栈帧包含了三个部分:

局部变量区(方法内基本类型变量,变量对象指针)

操作数栈区(存放方法执行过程中产生的中间结果)

运行环境区(动态连接、正确的方法返回相关信息、异常捕捉)

(10)本地方法栈

本地方法栈的功能和JVM栈非常类似,用于存储本地方法的局部变量表,本地方法的操作数栈等信息。

栈的存取类型为类似于水杯,先进后出。

栈内的数据在超出其作用域后,会被自动释放,它不由JVM GC管理。

每一个线程都是包含一个栈区,每个栈区的数据都是私有的,其他栈不能访问。

本地方法栈是在程序调用或JVM调用本地方法接口(Native)时候启用。

本地方法都不是使用Java语言编写,比如使用C语言编程写的本地方法,本地方法也不由JVM去运HotSpot VM 将本地方法栈和JVM栈合并了。

(11)程序计数器

在JVm的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一跳需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了各条线程之间的切换后计数器能恢复到正确的执行位置,所以每条线程都会有一个独立的程序计数器。

程序计数器仅能占很小的一块内存空间。

当线程正在执行一个Java方法,程序计数器记录的是正在执行的JVM字节码指令的地址。如果正在执行的是一个Native(本地方法),那么这个计数器的值则为空。

程序计数器这个内存区域是唯一一个在JVM规范没有规定任何OutOfMemoryError的区域。

(12)JVM执行引擎

Java 虚拟机相当于一台虚拟机“物理机”,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接建立在处理器、硬件、指令集合操作系统层面上的。而JVM的执行引擎是自己实现的,因此程序员可以自行制定指令集和执行引擎的结构体系,因此能够执行那些不被硬件直接支持的指令集格式。

在JVM规范中指定了虚拟机字节码执行引擎的概念模型,这个模型称之为JVM执行引擎的统一外观。JVM实现中,可能会有两种的执行方法:解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码)。有些虚拟机只采用一种执行方式,有些则可能同时采用两种,甚至有可能包含几个不同级别的编译器执行引擎。

输入的是字节码文件,处理过程是等效字节码解析过程,输出的是执行结果。在这三点上每个JVM执行引擎都是一致的。

(13)本地方法接口(JNI)

JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C和C++)。

JNI的适用场景

当我们有一些旧的库,已经使用C语言编写好了,如果要移植到Java上来,非常浪费时间,而JNI可以支持 ava程序与C语言编写的库进行交互,这样就不必要进行移植了。或者是与硬件、操作系统进行交互、提高程 序的性能等,都可以使用JNI。需要注意的一点是需要保证本地代码能工作在任何Java虚拟机环境。

JNI的副作用

一旦使用JNI,Java程序将丢失了Java平台的两个优点:

1、程序不再跨平台,要想跨平台,必须在不同的系统环境下程序编译配置本地语言部分。

2、程序不再是绝对安全的,本地代码的使用不当可能会导致整个程序崩溃。一个通用规则是,调用本地方法应该集中在少数的几个类当中,这样就降低了Java和其他语言之间的耦合。

(14)JVM GC(垃圾回收机制)

(15)JVM 常量池

JVM常量池也称之为运行时常量池,它是方法区(Method Area)的一部分。用于存放编译器期间生成的各种字面量和符号引用。运行时常量池不要求一定只有在编译器产生的才能进入,运行期间也可以将新的常量放入池中,这种特性被开发人员利用比较多的就是String.intern()方法。

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,它也实现了对象的共享。

例如字符串常量池:在编译阶段把所有字符串文字放到一个常量池中。

  • 节省内存空间:常量池中如果没有对应的字符串,那么则返回该对象的引用,从而不必再次创建一个新对象。

  • 节省运行时间:比较字符串时, ==比equals()快。对于两个引用变量,==判断引用是否相等,也就可以判断实际值是否相等。

    双等号(==)的含义

    基本数据类型之间使用双等号,比较的是数值。

    复合数据类型(类)之间使用双等号,比较的是对象的引用地址是否相等。

    八种基本数据类型的包装类和常量池

    Byte、Short、Integer、Long、Character、Boolean、String这七种包装类都各自实现了自己的常量池

        Integer i1 = 20;
        Integer i2 = 20;
        System.out.println(i1=i2);      //输出TRUE
    

    Byte、Short、Integer、Long、Character这五种包装类都默认创建了数值[-128, 127]的缓存数据。当对这5个类型的数据不在这个区间内的时候,将会去创建新的对象,并且不会将这些新的对象放入常量池中。

        public static Integer valueOf(int i) {
        if(i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    Integer i1 = 200;
    Integer i2 = 200;
    System.out.println(i1==i2);     //返回FALSE
    

    Float 和 Double 没有实现常量池

    String包装类与常量池

    String str1 = "aaa";
    

    当以上代码运行时,JVM会到字符串常量池查找"aaa"这个字面量对象是否存在?

    存在:则返回该对象的引用给变量str1

    不存在:则在堆中创建一个相应的对象,将创建的对象的引用存放到常量池中,同时将引用返回给str1

    String str1 = "aaa";
    String str2 = "aaa";
    System.out.println(str1 == str2);       //返回TRUE
    

    因为变量str1和str2都指向同一个对象,所以返回true。

    String str3 = new String("aaa");
    System.out.println(str1 == str3);       //返回FALSE
    

    当我们使用new来构造字符串对象的时候,不管字符串常量池中是否有相同内容的对象的引用,新的字符串对象都会创建。因为两个指向的是不同的对象,所以返回false。

    String.intern()方法

    对于使用了new创建的字符串对象,如果想要将这个对象引用到字符串常量池,可以使用intern()方法。

    调用intern()方法后,检查字符串常量池中是否有这个对象的引用,并做如下操作:

    存在:直接返回对象引用给变量。

    不存在:将这个对象引用加入到常量池,再返回对象引用给标量。

    String interns = str3.intern();
    System.out.println(interns == str1);  //返回TRUE</pre>

垃圾收集机制(Garbage collection)

stop - the - world,当stop - the - world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world的发生。

JVM GC 回收哪个区域类的垃圾

需要注意的是,JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC 的管理范围内。

JVM GC怎么判断对象可以被回收了

  • 对象没有引用

  • 作用域发生未捕获异常

  • 程序在作用域正常执行完毕

  • 程序执行了system.exit()

  • 程序发生意外终止(被杀线程等)

    在Java程序中不能显示的分配的注销缓存,因为这些事情JVM都帮我们做了,那就是GC。

有些时候我们可以将相关的对象设置成null 来试图显示的清空缓存,但是并不是设置为null就会一定被标记为可回收,有可能会发生逃避。

将对象设置null至少没有什么坏处,但是使用System.gc()便不可取了,使用System.gc()时候并不是马上执行GC操作,而是会等待一段时间,甚至不执行,而且System.gc()如果被执行,会触发Full GC,这非常影响性能。

JVM GC什么时候执行?

eden区空间不够存放新对象的时候,执行Minro GC。升到老年代的对象大于老年代剩余空间的时候执行Full GC,或者小于的时候被HandlePromotionFailure 参数强制Full GC 。调优主要是减少 Full GC 的触发次数,可以通过 NewRatio 控制新生代转老年代的比例,通过MaxTenuringThreshold 设置对象进入老年代的年龄阀值(后面会介绍到)。

按代的垃圾回收机制

新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后都会变得不可达,很多对象被创建在新生代;然后“消失”。对象从这个区域“消失”的过程称为:Minor GC.

老生代(Old generation):对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对于大的空间,发生在老生代的GC次数要比新生代少得多。对象从老生代中消失的过程,称之为:Major GC 或者 Full GC

持久代(Permanent generation)也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC。只不过在这个区域发生的条件非常严苛,必须符合以下三种条件才会被回收:

1、所有实例被回收

2、加载该类的 ClassLoader 被回收

3、Class 对象无法通过任何途径访问(包括反射)

如果老年代的对象需要引用新生代的对象,会发生什么呢?

为了解决这个问题,老年代中存在一个 card table,它是一个512byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询card table 来决定是否可以被回收,而不用查询整个老年代。这个 card table 由一个 write barrier 来管理。write barrier 给GC带来了很大的性能提升,虽然由此可能带来一些开销,但是完全是值得的。

默认的新生代(Young generation)、老年代(Old generation)所占空间比例为1:2

新生代空间的构成与逻辑

为了更好的理解GC,我们来学习新生代的构成,它用来保存那些第一次被创建的对象,它被分为三个空间:

  • 一个伊甸园空间(Eden)

  • 两个幸存者空间(From Survivor、To Survivor)

默认新生代空间的分配:Eden:From:To = 8:1:1

每个空间的执行顺序如下:

  1. 绝大多数刚刚被创建的对象会存放在伊甸园空间(Eden)。

  2. 在伊甸园空间执行第一次GC(Minor GC)之后,存活的对象被移动到其中一个幸存者空间(Survivor)。

  3. 此外,每次伊甸园空间执行Fc后,存活的对象会被堆积在同一个幸存者空间

  4. 当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。然后会清空已经饱和的哪个幸存者空间。

  5. 在以上步骤中重复N次(N = MaxTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会被移动到老年代。

从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。如果两个幸存者的空间都有数据,或两个空间都是空的,那一定是你的系统出现了某种错误。

我们需要重点记住的是,对象在刚刚被创建之后,是保存在伊甸园空间的(Eden)。那些长期存活的对象会经由幸存者空间(Survivor)转存到老年代空间(Old generation)

也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间)则直接进入到老年代,一般在Survivor 空间不足的情况下发生。

老年代空间的构成与逻辑

老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从Survivor 空间中熬过来的,它们绝不会轻易的狗带。因此,Full GC(Major GC)发生的次数不会有Minor GC 那么频繁,并且做一次Major GC 的时间比Minor GC 要更长(约10倍)。

JVM GC 算法讲解

1、根搜索算法

根搜索算法是从离散数学中的图论引入的,程序把所有引用关系看作一张图,从一个节点GC ROOT 开始,寻找对用的引用节点,找到这个节点后,继续寻找这个几点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

GC-ROOT.jpg

上图红色的为无用的节点,可以被回收。

目前Java中可以作为GC ROOT的对象有:

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

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

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

  4. 本地方法栈中引用的对象(Native对象)

基本所有GC算法都引用根搜索算法这种概念。

2、标记 - 清除算法

标记-清除算法.png

标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收,如上图。

标记-清除算法不需要进行对象的移动,并且仅对不存活对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对存活的对象进行整理,因此会导致内存碎片。

3、复制算法

复制算法.jpg
复制算法将内存划分为两个区间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空间区间)则是空闲的。

复制算法采用从根集合扫描,将存活的对象复制到空闲去接,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。

复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服50%内存的浪费。

4、标记 - 整理算法

标记-整理算法.png
标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象用的空间后,会将所有的对象往左端空闲空间移动,并更新对应的指针。标记-整理 算法是在 标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收(Minor GC)主要采用复制算法。而对于老年代的回收(Major GC),大多采用标记-整理算法

垃圾回收器简介

需要注意的是,每一个回收器都存在Stop The World 的问题,只不过各个回收器在Stop The World 时间优化程度、算法的不同,可根据自身需求选择适合的回收器。

1、Serial(-XX:+UseSerialGC)

这是一个串行收集器,Serial收集器是Java虚拟机中最基本、历史最悠久的收集器。在JDK1.3之前是虚拟机新生代收集器的唯一选择。Serial收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束

使用算法:复制算法

Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。

2、SerialOld(-XX:+UseSerialGC)

SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。

使用算法:标记 - 整理算法

3、ParNew(-XX:+UseParNewGC)

ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工作。

使用算法:复制算法

ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器,所以一定要注意使用场景。

4、ParallelScavenge(-XX:+UseParallelGC)

ParallelScavenge又被称为吞吐量优先收集器,和ParNew 收集器类似,是一个新生代收集器

使用算法:复制算法

ParallelScavenge收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99% 。

5、ParallelOld(-XX:+UseParallelOldGC)

ParallelOld是并行收集器,和SerialOld一样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6之后才开始提供的,在此之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实!

使用算法:标记 - 整理算法

6、CMS (-XX:+UseConcMarkSweepGC)

CMS是一个老年代收集器,全称 Concurrent Low Pause Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.5、1.6中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用CMS非常合适。

CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。

使用算法:标记 - 清理

CMS的执行过程如下:

初始标记(STW initial mark)

在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。

并发标记(Concurrent marking)

这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程哦。

并发预清理(Concurrent precleaning)

这个阶段任然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。

重新标记(STW remark)

这个阶段会再次暂停正在执行的应用线程,重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。

并发清理(Concurrent sweeping)

这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。

并发重置(Concurrent reset)

这个阶段任然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS的缺点:

1、内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。不过CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。

2、需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。

3、需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。

这是一个新的垃圾回收器,既可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了这个回收器,Sun公司预期SunHotSpot1.7发布正式版本。通过重新划分内存区域,整合优化CMS,同时注重吞吐量和响应时间。杯具的是Oracle收购这个收集器之后将其用于商用收费版收集器。因此目前暂时没有发现哪个公司使用它,这个放在之后再去研究吧。

新代收集器:

Serial (-XX:+UseSerialGC)

ParNew(-XX:+UseParNewGC)

ParallelScavenge(-XX:+UseParallelGC)

G1 收集器

SerialOld(-XX:+UseSerialOldGC)

ParallelOld(-XX:+UseParallelOldGC)

CMS(-XX:+UseConcMarkSweepGC)

相关文章

  • 初识Java

    Java语言概述 java语言概述Java是Sun公司开发的一门编程语言,目前被Oracle公司收购,编程语言就是...

  • 双11Java程序员书单推荐

    Java 《Java核心技术卷I》 《Java核心技术卷II》 《Java编程思想》 《Java并发编程实战》 《...

  • Java 核心技术 - Java编程语言概述(二)

    1.1.4 Java核心机制与JVM运行原理 Java虚拟机(Java Virtual Machine) (1)J...

  • Java编程入门

    第一单元:java平台概述 JAVA语言: 跟任何编程语言一样,Java 语言拥有自己的结构、语法规则和编程范例。...

  • 有追求的程序员书单

    Java经典进阶书籍 Effective Java Java编程思想 Java并发编程实战 Java核心技术卷一 ...

  • Java基础语法_Day01

    一、Java开发环境搭建 Java概述 众所周知Java是一门编程语言,编程语言就是用来编写软件的。那么使用Jav...

  • Java 环境搭建

    1 Java概述   Java是一门编程语言,编程语言就是用来编写软件的。那么使用Java到底能用来编写什么软件呢...

  • Java 核心技术 - Java编程语言概述(一)

    1.1.1 计算机语言介绍 ​计算机编程语言:包括机器语言、汇编语言、高级语言。​高级语言:是用二进制代码表示的计...

  • Java 核心技术 - Java编程语言概述(三)

    1.1.5 搭建Java开发环境 1.1.6 JDK 的安装与配置 1.1.7 开发第一个Java应用程序 1.1...

  • Day01_java概述

    二、Java语言介绍 2.1 Java语言概述 Java语言是SUN公司(Stanford University ...

网友评论

      本文标题:Java 核心技术 - Java编程语言概述(二)

      本文链接:https://www.haomeiwen.com/subject/cvhqpctx.html