JAVA

 

目录

JAVA

1.     历史

1.1.         发展历史

1.2.         特点

2.     数据特征

2.1.         基础语法

2.1.1.    基础语法

2.1.2.    基础数据类型

2.1.3.    引用数据类型

2.2.         面向对象

2.2.1.    对象-(待补充)

2.2.2.    异常处理

2.2.2.1.   Throwable-(待补充)

2.2.2.2.    try-with-resources-(待补充)

2.2.         逻辑结构

2.2.1.    循环

2.2.2.    跳转

2.2.3.    条件

3.     运行特征

3.1.         线程

3.1.1.    内核线程

3.1.2.    用户线程

3.1.2.1.    线程生命周期

3.1.2.2.    线程协作

3.1.2.2.1.   

3.1.2.2.2.    锁升级

3.1.2.2.3.    线程同步

3.2.         内存

3.2.1.    共享内存-(待补充)

3.2.2.    工作内存-(待补充)

3.2.3.    内存结构

3.2.3.1.    寄存器

3.2.3.2.    堆栈

3.2.3.3.   

3.2.3.4.    静态存储

3.2.3.5.    常量存储

3.2.3.6.    RAM存储

3.2.3.7.    内存分配

3.2.3.8.    happens-before原则-(待补充)

3.2.3.9.    as-if-serial原则-(待补充)

4.     垃圾回收

4.1.         垃圾回收历史

4.1.1.    历史

4.1.2.    标记算法

4.1.3.    回收算法

4.1.4.    三色算法-(待补充)

4.1.5.    并行度

4.2.         垃圾回收器说明

4.2.1.    指定垃圾收集器

4.2.2.    调整堆大小

4.2.3.    收集gc日志

4.2.4.    CMS垃圾收集器参数

4.2.5.    其他参数

5.     参考文献

 

1.  历史

1.1.   发展历史

 

1990年,由于单片式计算机系统的火爆,sun公司为了抢占市场,成立了包括詹姆斯·高斯林在内的green小组,主要攻克家电的嵌入式应用,这些家电由于当时硬件的限制,计算处理能力和内存都非常有限,这就要求在极为紧凑的硬件中必须写入大量代码,且当时的嵌入式处理器芯片的种类繁杂,这对跨平台性要求极高,所以采用的语言必须具备简单、可移植、高性能等特点。当时最流行的语言是C++,所以green小组优先考虑使用C++编写程序,但是在实践过程中却发现单片机的硬件资源极其匮乏,而C++有过于庞大复杂。于是green小组萌生了开发一门实用性更强的语言的想法。green小组在C++的基础上进行了改良,去除了不太实用及影响安全的成分,并结合嵌入式系统的实时性要求,开发了一种称为Oak的面向对象语言,这就是java的前身。

 

1992年,green小组向硬件生产商展示采用Oak语言开发的Green操作系统,可是硬件生产商觉得对Oak语言一无所知,贸然生产风险太大,所以Oak语言坐上了冷板凳。

1993年,美国伊利诺州的伊利诺大学的NCSA组织,发表了第一个可以显示图片的浏览器,命名为MOSAIC浏览器,这是第一个万维网浏览器。

1994年,green小组受到Mosaic浏览器的启发,认为互联网应用前景巨大,改变了面向硬件编程的目标,转而研究怎么将技术运用于互联网,并开发了一个小型万维网浏览器WebRunner

1995年,互联网蓬勃发展,许多公司看到了互联网的前景,纷纷投入大量人力、物力研究。这时Sun公司想到了冷板凳上的Oak,重新审视了一番Oak,发现其开发的应用特性刚好满足互联网的需求,所以加大力度开发了可以嵌入网页并且可以随同网页在网络上传输的Applet程序,并准备为Oak申请注册商标,但是发现该商标已被注册。一群程序员在那里左思右想,不知道该起什么名字,一人在看到咖啡的时候灵机一动,提议道:要不就叫java,其他人都表示赞同,于是Oak改名为JAVA,并与同年523Sun world会议上正式发布JavaHotJava浏览器。JAVA就此诞生!

 

1995523日,Sun公司在Sun world会议上正式发布JavaHotJava浏览器,宣告着JAVA的诞生,创始人为詹姆斯·高斯林

1996年,Sun公司正式发布独立的、可供下载的java开发工具JDK1.0版本,这是Java发展历程中的重要里程碑

1997年,JDK1.1发布,3周内下载量达22万次

1998年,第二代java平台发布,并发布了企业版java EE

1999年,Sun公司发布Java第二代平台的三大版本:标准版(J2SE):桌面级 C/S,企业版(J2EE):企业级 B/S,微型版(J2ME):移动端

2000年,JDK1.3JDK1.4J2SE1.3相继发布,几周后其获得了Apple公司Mac OS X的工业标准的支持

2001年,J2EE1.3发布

2002年,J2SE1.4发布,自此Java的计算能力有了大幅提升

2004年,J2SE1.5发布,J2SE 1.5更名为Java SE 5.0(内部版本号1.5.0),成为Java语言发展史上的又一里程碑

2005年,在Java One大会上,Sun公司发布了Java SE 6。至此,Java的各种版本已经更名,已取消其中的数字2,如J2EE更名为JavaEEJ2SE更名为JavaSEJ2ME更名为JavaME

2009年,oracle公司收购了sun公司

2010年,Java编程语言的共同创始人之一詹姆斯·高斯林从Oracle公司辞职

2011年,oracle公司举行了全球性的活动,以庆祝Java7的推出,随后Java7正式发布

2014年,oracle公司发布了Java8正式版

2017年,oracle公司发布Java SE 9

20183月,oracle公司发布Java SE 10

20189月,oracle公司发布Java SE 11

20192月,oracle公司发布Java SE 12

20199月,oracle公司发布Java SE 13

20203月,oracle公司发布Java SE 14

20209月,oracle公司发布Java SE 15

20214月,oracle公司发布Java SE 16

1.2.   特点

 

Java 语言是简单的:

 

Java 语言的语法与 C 语言和 C++ 语言很接近,使得大多数程序员很容易学习和使用。另一方面,Java 丢弃了 C++ 中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。特别地,Java 语言不使用指针,而是引用。并提供了自动分配和回收内存空间,使得程序员不必为内存管理而担忧。

 

 

Java 语言是面向对象的:

 

Java 语言提供类、接口和继承等面向对象的特性,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为 implements)。Java 语言全面支持动态绑定,而 C++语言只对虚函数使用动态绑定。总之,Java语言是一个纯的面向对象程序设计语言。

 

 

Java语言是分布式的:

 

Java 语言支持 Internet 应用的开发,在基本的 Java 应用编程接口中有一个网络应用编程接口(java net),它提供了用于网络应用编程的类库,包括 URLURLConnectionSocketServerSocket 等。Java RMI(远程方法激活)机制也是开发分布式应用的重要手段。

 

Java 语言是健壮的:

Java 的强类型机制、异常处理、垃圾的自动收集等是 Java 程序健壮性的重要保证。对指针的丢弃是 Java 的明智选择。Java 的安全检查机制使得 Java 更具健壮性。

 

 

Java语言是安全的:

 

Java通常被用在网络环境中,为此,Java 提供了一个安全机制以防恶意代码的攻击。除了Java 语言具有的许多安全特性以外,Java 对通过网络下载的类具有一个安全防范机制(类 ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代码检查,并提供安全管理机制(类 SecurityManager)让 Java 应用设置安全哨兵。

 

 

Java 语言是体系结构中立的:

 

Java 程序(后缀为 java 的文件)在 Java 平台上被编译为体系结构中立的字节码格式(后缀为 class 的文件),然后可以在实现这个 Java 平台的任何系统中运行。这种途径适合于异构的网络环境和软件的分发。

 

Java 语言是可移植的:

 

这种可移植性来源于体系结构中立性,另外,Java 还严格规定了各个基本数据类型的长度。Java 系统本身也具有很强的可移植性,Java 编译器是用 Java 实现的,Java 的运行环境是用 ANSI C 实现的。

 

 

Java 语言是解释型的:

 

如前所述,Java 程序在 Java 平台上被编译为字节码格式,然后可以在实现这个 Java 平台的任何系统中运行。在运行时,Java 平台中的 Java 解释器对这些字节码进行解释执行,执行过程中需要的类在联接阶段被载入到运行环境中。

 

 

Java 是高性能的:

 

与那些解释型的高级脚本语言相比,Java 的确是高性能的。事实上,Java 的运行速度随着 JIT(Just-In-Time)编译器技术的发展越来越接近于 C++

 

 

Java 语言是多线程的:

 

Java 语言中,线程是一种特殊的对象,它必须由 Thread 类或其子(孙)类来创建。通常有两种方法来创建线程:其一,使用型构为 Thread(Runnable) 的构造子类将一个实现了 Runnable 接口的对象包装成一个线程,其二,从 Thread 类派生出子类并重写 run 方法,使用该子类创建的对象即为线程。值得注意的是 Thread 类已经实现了 Runnable 接口,因此,任何一个线程均有它的 run 方法,而 run 方法中包含了线程所要运行的代码。线程的活动由一组方法来控制。Java 语言支持多个线程的同时执行,并提供多线程之间的同步机制(关键字为 synchronized)。

 

 

Java 语言是动态的:

 

Java 语言的设计目标之一是适应于动态变化的环境。Java 程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类。这也有利于软件的升级。另外,Java 中的类有一个运行时刻的表示,能进行运行时刻的类型检查。

2.  数据特征

2.1.   基础语法

2.1.1.     基础语法

(待补充)。 

2.1.2.    基础数据类型

Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。

byte

byte 数据类型是8位、有符号的,以二进制补码表示的整数;

最小值是 -128-2^7);

最大值是 1272^7-1);

默认值是 0

byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一;

例子:byte a = 100byte b = -50

short

short 数据类型是 16 位、有符号的以二进制补码表示的整数

最小值是 -32768-2^15);

最大值是 327672^15 - 1);

Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一;

默认值是 0

例子:short s = 1000short r = -20000

int

int 数据类型是32位、有符号的以二进制补码表示的整数;

最小值是 -2,147,483,648-2^31);

最大值是 2,147,483,6472^31 - 1);

一般地整型变量默认为 int 类型;

默认值是 0 

例子:int a = 100000, int b = -200000

long

long 数据类型是 64 位、有符号的以二进制补码表示的整数;

最小值是 -9,223,372,036,854,775,808-2^63);

最大值是 9,223,372,036,854,775,8072^63 -1);

这种类型主要使用在需要比较大整数的系统上;

默认值是 0L

例子: long a = 100000Llong b = -200000L
"L"
理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。

float

float 数据类型是单精度、32位、符合IEEE 754标准的浮点数;

float 在储存大型浮点数组的时候可节省内存空间;

默认值是 0.0f

浮点数不能用来表示精确的值,如货币;

例子:float f1 = 234.5f

double

double 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数;

浮点数的默认类型为 double 类型;

double类型同样不能表示精确的值,如货币;

默认值是 0.0d

 

例子:

 

double   d1  = 7D ;double   d2  = 7.; double   d3  =  8.0; double   d4  =  8.D; double   d5  =  12.9867;

 

7 是一个 int 字面量,而 7D7. 8.0 double 字面量。

 

boolean

boolean数据类型表示一位的信息;

只有两个取值:true false

这种类型只作为一种标志来记录 true/false 情况;

默认值是 false

例子:boolean one = true

char

char 类型是一个单一的 16 Unicode 字符;

最小值是 \u0000(十进制等效值为 0);

最大值是 \uffff(即为 65535);

char 数据类型可以储存任何字符;

例子:char letter = 'A';

 

2.1.3.    引用数据类型

引用数据类型是基础数据类型的对应的包装类。

 

 

 

2.2.   面向对象

2.2.1.    对象-(待补充)

Java作为一种面向对象语言。支持以下基本概念:

多态

继承

封装

抽象

对象

实例

方法

重载

本节我们重点研究对象和类的概念。

对象:对象是类的一个实例(对象不是找个女朋友),有状态和行为。

类:类是一个模板,它描述一类对象的行为和状态。

 

2.2.2.    异常处理

Java 异常处理

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。

异常发生的原因有很多,通常包含以下几大类:

用户输入了非法数据。

要打开的文件不存在。

网络通信时连接中断,或者JVM内存溢出。

这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。-

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:

检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。


Exception 类的层次

所有的异常类是从 java.lang.Exception 类继承的子类。

Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error

Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。

Error 用来指示运行时环境发生的错误。

例如,JVM 内存溢出。一般地,程序不会从错误中恢复。

异常类有两个主要的子类:IOException 类和 RuntimeException 类。

 

2.2.2.1.         Throwable-(待补充)

Throwable

2.2.2.2. try-with-resources-(待补充)

 

try-with-resources

 

 

 

2.2.   逻辑结构

 

2.2.1.    循环

顺序结构的程序语句只能被执行一次。

如果您想要同样的操作执行多次,就需要使用循环结构。

Java中有三种主要的循环结构:

while 循环

do…while 循环

for 循环

 

2.2.2.    跳转

 

2.2.3.    条件

if...else语句

if 语句后面可以跟 else 语句,当 if 语句的布尔表达式值为 false 时,else 语句块会被执行。

语法

if…else 的用法如下:

if(布尔表达式){ //如果布尔表达式的值为true }else{ //如果布尔表达式的值为false }

 

 

3.  运行特征

3.1.   线程

3.1.1.    内核线程

 

3.1.2.    用户线程

 

3.1.2.1.         线程生命周期

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期。

IMG_256

新建状态:

使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

 

就绪状态:

当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

 

运行状态:

如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

 

阻塞状态:

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

 

 

等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

 

 

同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)

 

 

其他阻塞:通过调用线程的 sleep() join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

 

死亡状态:

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

 


线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 Thread.MIN_PRIORITY - 10 Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。


创建一个线程

Java 提供了三种创建线程的方法:

通过实现 Runnable 接口;

通过继承 Thread 类本身;

通过 Callable Future 创建线程。

3.1.2.2.         线程协作

3.1.2.2.1.  
3.1.2.2.2.     锁升级
3.1.2.2.3.     线程同步

 

 

3.2.   内存

3.2.1.    共享内存-(待补充)

 

 

3.2.2.    工作内存-(待补充)

 

3.2.3.    内存结构

3.2.3.1.  寄存器

 

 

(1) 寄存器(register)。这是最快的保存区域,这是主要由于它位于处理器内部。然而,寄存器的数量十分有限,所以寄存器是需要由编译器分配的。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。

3.2.3.2.  堆栈

 

(2) 堆栈(stack)。位于通用RAM随机访问存储器)中。可通过它的堆栈指针获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的长度以及存在时间。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java 数据要保存在堆栈里— — 特别是对象句柄(也称对象的引用),但Java对象并不放到其中。

3.2.3.3. 

 

(3) (heap)。一种通用性的内存池(也在RAM区域),其中保存了Java对象。和堆栈不同的是,内存堆Heap )最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new 命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价。在堆里分配存储空间时会花掉更长的时间。

3.2.3.4.  静态存储

 

(4) 静态存储(static storage)。这儿的静态Static)是指位于固定位置(尽管也在RAM 里)。程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java 对象本身永远都不会置入静态存储空间。

3.2.3.5.  常量存储

 

(5) 常数存储(constant storage)。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。

3.2.3.6.  RAM存储

 

(6) RAM 存储(non-storage-RAM)。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是流式对象固定对象。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。Java 1.1提供了对轻量级持久化(Lightweight persistence)的支持。未来的版本甚至可能提供更完整的方案。

 

 

 

3.2.3.7.  内存分配

 

Java内存模型结构

首先说一下运行时数据区,主要分为两类

线程共享区,主要包含方法区和堆

线程私有区,主要包含 虚拟机栈,本地方法栈,程序计数器

对于每一个线程来说,栈都是私有的,堆是共享的,所以栈中的变量,比如局部变量,方法定义参数、异常处理器参数,它们都不是共有的,所以内存之间是不可见的,所以也不收内存模型影响,而堆中的变量时共享的,所以又叫共享变量, 内存可见性是针对共享变量

堆中一定就可见性吗

堆中也会出现内存不可见的问题,怎么产生的呢❓这是因为现代计算机为了高效,往往会在高速缓存区中缓存共享变量,因为cpu访问缓存区比访问内存要快得多。

所谓内存不可见性,就是线程对某个共享变量在线程自己的缓冲中存在副本的时候对主内存中共享变量的值是不可见的,看不见主存中的值。线程操作一个共享变量时,它首先从主存中拉取并复制一份变量放置到自己的工作内存中,然后在工作内存中对变量进行修改,处理完之后将工作内存中的值重新写回到主存中。所以在这个过程中,如果在缓存失效之前立即命中,就会导致更新过的主存中值不一致的问题

Java中,每个线程都有自己的本地内存,存储了该线程以读、写共享变量的副本,它是一个抽象的概念,线程之间的通信有内存模型控制,简称JMM

所有的共享变量都存在主内存中。

每个线程都保存了一份该线程使用到的共享变量的副本。

所以「线程间通信必须经过主内存」,JMM规定,「线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取」。

Java中的volatile关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字不仅保证可见性,同时也保证了原子性(互斥性)。在更底层,JMM通过内存屏障来实现内存的可见性以及禁止重排序。为了程序员的方便理解,提出了happens-before,它更加的简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法。

 

3.2.3.8.  happens-before原则-(待补充)

 

3.2.3.9.  as-if-serial原则-(待补充)

 

4.  垃圾回收

4.1.   垃圾回收历史

4.1.1.    历史

 

 

所以在垃圾回收器的发展历史中,出现了两个分区方式(根据分区的粒度不同)

 

分代内存布局:整个java进程的内存空间分成量大块:新生代和老年代。每一个分代中的内存地址是连续的。新生代里存放生命期端的对象,对应的垃圾回收的时候就用复制算法;而老年代里存放生命期比较长的对象,对应的垃圾回收的时候就使用整理算法。因为新生代使用复制算法,随意新生代需要进一步划分三部分,来满足复制算法:eden区、surviver from区、suviver to区。

分区内存布局:java进程的内存分成nregion,回收的时候也按region回收。

 

 

4.1.2.    标记算法

标记算法:

 

引用计数:每个对象都有一个计数器,被别的对象引用时计数器加1,当技术器变成1的时候,这个对象就是垃圾了。这个实现 简单且高效,但是存在循环引用不好处理的问题。

根可达算法:有一个根列表(对于java,比如运行栈、常量区、jni栈等都是根),通过根去遍历,能够遍历的就是存活的对象、不能遍历到的就是垃圾了。

4.1.3.    回收算法

回收算法:

 

清理:只是清理垃圾对象,不作内存整理。所以这种回收方式会产生内存随便,对应的内存管理也就只能使用链表方式,且回收后对象的内存地址是不变的。

整理:将垃圾对象回收后,会整理内存。正式因为整理过程,停顿时间就比较长。这种方式可以使用指针碰撞方式来管理内存

复制:将内存一分为二,回收的时候是将存活的对象copy到另外一块内存,然后释放原来那块内存整体释放。这种方式没有内存碎片、效率也比整理要高,但是内存的使用率就变低了,有一半的内存都是浪费的,并且当内存很大的时候、存活对象很多的时候,copy过程就会很长,进而停顿时间就会变长

综合来看,复制算法适合在垃圾回收执行时存活对象比较少的场景;整理算法适合在垃圾回收时存活对象较多但是不能有内存碎片的场景;而清理算法因为会产品内存碎片,实际的场景其实并不多,CMS使用该算法,但也诟病比较多。

 

对于一个java进程中的对象,经过统计发现,大部分对象生命期都是比较短的,活不过一次垃圾回收,而少部分生命期比较长,多次gc后依然存在,甚至和java进程生命期相同。针对这种情况,就对java的内存进行了分区,不同分区使用不同的回收算法,来达到最大的收集效果。

4.1.4.    三色算法-(待补充)

回收算法:

 

4.1.5.    并行度

从并行的角度看垃圾回收的发展。

 

gc线程回收:这个时期,gc线程只有一个,当启动垃圾回收的时候,暂停所有业务线程,然后gc线程开始工作,经过标记、回收后,业务线程才能继续运行。这个垃圾回收过程业务线程是完全暂停的(STWstop the world)。早期因为java进程内存都比较小,业务也并不复杂,所以垃圾回收都是单线程的,停顿时间也是可接受的,早期的SerialSerial Old都是单线程的垃圾回收器

 

 

gc线程回收:随着java进程的内存越来越大,单线程回收效率变的很慢了,所以这个时候自然的一个想法就是将gc线程变成多线程的,多线程来并行的标记-回收内存,会更快一些。但是在垃圾回收的过程中,业务线程还是完全暂停的。

 

 

gc线程和业务线程并行回收:随着技术发展,java的内存需求越来越大、对于业务线程的停顿时间要求也越来越高。要进一步缩短停顿时间,那就是将gc过程进一步系分,在最必要的时候才STW,其他步骤业务线程和gc线程并行运行,从而减少STW时间。例如CMS

 

 

所以到了现在的垃圾回收器,就是多种回收算法的组合、多gc线程、以及gc过程中某些阶段gc线程和业务线程可并行运行的有机结合,来提高gc的性能的。比如CMSG1,以及后来的ZGC

 

4.2.   垃圾回收器说明

 

 

4.2.1.    指定垃圾收集器

-XX:+UseSerialGC 指定使用串行垃圾收集器,新生代及老年代都是串行收集,在大堆或者多核cpu的环境中不大适合使用该种垃圾收集器

-XX:+UseParallelGC,并行垃圾收集器,新生代使用并行收集,老年代使用串行收集

-XX:+UseParallelOldGC,并行垃圾收器,新生代使用并行手机,老年代使用并行收集,在一些低版本的JVM该参数不支持或者不生效,这两种垃圾收集适合不在乎延时需要高吞吐的环境下使用,比如说一些批处理程序,

-XX:+UseConcMarkSweepGC,并发垃圾收集,新生代使用并行收集,老年代采用并发收集,适合低延迟应用使用该种垃圾器。使用并发垃圾收集,相当于自动添加了参数-XX:+UseParNewGC

-XX:+UseG1GC,使用G1垃圾收集,该种垃圾收集器比较复杂,目前在此种垃圾收集的调优经验较少,在需要大堆的应用可以考虑使用,相比并发垃圾收集器,其产生的碎片更少,且可以通过参数控制垃圾收集过程中的停顿时间。

4.2.2.    调整堆大小

在描述调整堆大小参数,先用一张图描述JVM堆的分布情况:

 

 

 

其中新生代由Edens0s1组成,有时候s0也叫做froms1叫做to。现代JVM都是将堆分成几个不同的区间,划分的方法主要是根据对象的存活时间长短将整个jvm内存分为新生代,老年代、永久代。相应的主要参数有:

 

-Xms[g|m|k]:调整堆的最小大小,比如:-Xms1G,表示堆最小为1G,在系统启动阶段JVM直接向系统申请1G内存,此参数相当于参数-XX:InitialHeapSize=n[g|m|k]

-Xmx[g|m|k]:调整堆的最大大小,比如:-Xms1G表示堆最大为1G,此参数相当于参数-XX:MaxHeapSize=n[g|m|k]

-XX:MinHeapFreeRatio=n,指定堆最小空余比例,当堆中空余内存比例小于该参数指定值时,JVM将增大内存大小直到最大堆大小,默认40

-XX:MaxHeapFreeRatio=n,指定堆最大空余比例,当堆中空余比例大于该参数制定值,JVM将减少内存大小,直到最小堆大小,默认为70

JVM将在发生FGC之后,根据这两个参数调整整个对的大小,FGC一般消耗的时间较多,为了降低延迟,一般将最小堆大小和最大堆大小设置成一样的。

-XX:NewRatio=n,老年代与新生代内存占比,-XX:NewRatio=4表示年轻代是老年代内存的1/4,也就是说年轻代占用总堆内存的1/5;

-XX:SurvivorRatio=n,Eden区与1survivor区域内存占比,-XX:SurvivorRatio=8,表示EdenEden区域的内存是一个survivor区域内存的8倍,一共两个survivor区域,因此survivor占用新生代的1/10

-XX:PermSize=[g|m|k],-XX:MaxPermSize=[g|m|k],这两个参数用于制定永久代内存大小,这个两个参数在jdk8中已经失效;

-XX:MetaspaceSize=[g|m|k], -XX:MaxMetaspaceSize=[g|m|k],在jdk中使用这两个参数指定元数据空间大小;

-XX:TargetSurvivorRatio=n,设定survivor区的目标使用率,默认是50%

-XX:MaxDirectMemorySize=[g|m|k],设置直接内存的大小,在使用一些nio框架时最好设置一下此参数,此参数如果过小也有可能导致频繁触发FGC

-Xss[g|m|k],指定Java线程栈的大小,默认值受环境影响,Xss越大,进程能运行的最大线程数就少,如果设置的过小,容易导致StackOverflowError错误;

4.2.3.    收集gc日志

-verbose:gc,输出gc日志信息,默认输出到标准输出,参数与-XX:+PrintGCDetails作用一致;

-XX:+PrintGCDetails,打印gc日志,打印的出日志包含日志收集原因,歌区域变化情况,以及用时;

-XX:+PrintGCDateStamps,日志中输出时间戳;

-XX:+PrintGCTimeStamps,日志中输出时间戳,与PrintGCDateStamps参数不同的地方在于,此参数输出的时间是相对与应用启动时间的差值;

-Xloggc:filename,gc日志信息输入到指定文件中,

-XX:+PrintGCApplicationStoppedTime,打印垃圾收集期间应用被暂停的时间;

-XX:+PrintGCApplicationConcurrentTime,垃圾收集之前打印出应用未中断的执行时间;

-XX:+UseGCLogFileRotation, -XX:GCLogFileSize=n,这两个参数用于设置gc文件滚动和设置滚动日志文件的个数,为了防止单个gc日志文件太大,生产上建议加上这两个参数;

4.2.4.    CMS垃圾收集器参数

调优CMS垃圾收集器比调优吞吐量垃圾收集器复杂许多,下面列出一些用于设置CMS收集器的参数:

 

-XX:CMSInitiatingOccupancyFraction=n,设置CMS垃圾收集器启动回收老年代的时机,当老年代对象占比超过n时,就启动一次CMS回收周期,注意:这个参数只是设定首次CMS垃圾回收;

-XX:+UseCMSInitiatingOccupancyOnly,使用这个参数可以使CMS一直按照CMSInitiatingOccupancyFraction设定的值启动;

-XX:CMSInitiatingPermOccupancyFraction=n,设置永久代内存占比超过n启动cms回收,此参数需要配合参数-XX:+CMSClassUnloadingEnabled一起使用(Java8默认开启);

-XX:+CMSParallelInitialEnabled, 用于开启CMS initial-mark阶段采用多线程的方式进行标记,用于提高标记速度,在Java8开始已经默认开启;

-XX:+CMSParallelRemarkEnabled,用户开启CMS remark阶段采用多线程的方式进行重新标记,默认开启;

-XX:CMSFullGCsBeforeCompaction,我们知道cms垃圾收集器会产生内存碎片,该参数用于设置多少次CMS gc之后进行一次内存压缩;该参数默认值为0

-XX:+ExplicitGCInvokesConcurrent -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses这两个参数用户指定hotspot虚拟在执行System.gc()时使用CMS周期;

-XX:+CMSScavengeBeforeRemark,强制hotspot虚拟机在cms remark阶段之前做一次minor gc,用于提高remark阶段的速度;

-XX:ConcGCThreads=n,设置并发垃圾收集的线程数,默认该值是基于ParallelGCThreads计算出来的;

-XX:+CMSPrecleaningEnabled,指定CMS是否需要进行Pre cleaning这个阶段,这个阶段的主要工作是:1、在并发标记阶段阶段由于业务线程同步在跑,新生代中对象有可能引用到老年代的对象,产生新的引用关系,这个时候需要重新标记出老年代中被引用的对象;2、老年代中对象引用关系发生变化,这个需要把引用关系发生变化的卡表(card table)标记为dirty,提高remark标记的效率;

-XX:CMSMaxAbortablePrecleanTime=n,设置cms abortable pre阶段最长持续时间,单位为s,默认值5sabortable prelean阶段默认在Eden区大于2M启动(通过参数:-XX:CMSScheduleRemarkEdenSizeThreshold =n设置),直到Eden区域占用率超过50%(通过参数:-XX:CMSScheduleRemarkEdenPenetration=n)时结束,同时还可以指定参数-XX:CMSMaxAbortablePrecleanLoops=n设置该阶段的循环次数(默认0,表示不限制次数),为了防止abortable pre clean阶段陷入无限等待,故设置一个最长的持续时间,达到最长持续时间之后,该阶段终止,进入remark阶段;

4.2.5.    其他参数

-XX:+DisableExplicitGC,禁止hotspot执行System.gc(),默认禁用;

 

-XX:+ScavengeBeforeFullGC,在执行full gc之前执行一次minor gc,默认开启;

 

-XX:MaxTenuringThreshold=n,设定新生代的对象在经历多少次minor gc之后将转移到老年代,默认是15

 

-XX:+HeapDumpOnOutOfMemoryError,在hotspot发生OOM时打印出堆,默认打印到应用工作目录,文件名称为java_pid%p.hprof

 

-XX:HeapDumpPath=xx,指定hotspot虚拟机OOM时堆转储文件的path

 

-XX:ErrorFile=xx,设置JVM crash时生成crash文件的路径,默认为./hs_err_pid%p.log

 

-XX:+ParallelRefProcEnabled,采用多线程的方式发现需要处理的finalize方法的对象,非多线程执行对象的finalize方法;

 

-XX:ParallelGCThreads=n,设置并行收集的线程数,默认值采用Runtime.getRuntime().availableProcessors()来确定。不过是建立在返回值小于等于8的情况下,反之,会使用Runtime.availableProcessors()*5/8作为线程数。

 

-XX:+UseAdaptiveSizePolicy,开启自适应调整JAVA内存策略,在使用吞吐量垃圾收集器时,该参数用于让jvm自行调整Eden区和Survivor区空间的大小,新生代与老年代空间的大小;

 

-XX:+PrintAdaptiveSizePolicy,可以打印出survivor的一些详细信息,关于survivor区间是否溢出,是否有对象转移到老年代;

 

-XX:+PrintTenuringDistribution,显示出survivor区间有效对象的年龄分布情况,可以通过该参数的输出确定出survivor大小以及MaxTenuringThreshold的值;

 

-XX:MaxGCPauseMillis=n,设置最大gc停顿时间,这个时间不是设置的越小越好,此参数只在ps收集器上有效,不建议修改此参数的值;

 

-XX:GCTimeRatio,设置垃圾收集时间占总时间的比率,默认为99,也就是说垃圾收集时间占用总时间的1%,此参数只在ps收集器上有效;

 

-XX:+HeapDumpAfterFullGC,在执行一次FGC之后打印出Heap到文件;

 

-XX:+HeapDumpBeforeFullGC,在执行一次FGC之前打印出Heap到文件,这两个参数主要用于调试;

 

-XX:+PrintHeapAtGC,打印gc前后堆详细信息,不管是minor gc还是full gc都会打印;

 

-XX:+GCLockerInvokesConcurrent,在执行gc之前都需要先先查看 gc locker是否被java线程持有,如果存在gc locker被持有的情况则忽略此次gc,在所有java线程完全释放gc locker之后补偿一次gc。此参数指定,如果是fgc的话,则补偿一次CMS back groud fgc

 

-XX:ReservedCodeCacheSize=[g|m|k]-XX:InitialCodeCacheSize=[g|m|k],指定代码缓存的大小,用于保存已编译方法生成的本地代码,如果代码缓存被占满JVM会发出警告信息,并切换到interpreted-only模式,JIT编译器被停用,字节码将不会再编译成机器码。这样的话对JVM的性能影响很大;

 

-XX:+UseCodeCacheFlushing,如果代码缓存不断增长导致代码缓存空间不够,使用该参数让jvm放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况;

 

-XX:+DoEscapeAnalysis,开启逃逸分析,逃逸分析是一种分析对象范围的技术,在一些情况一个线程分配的对象可能会被其他对象使用,这种现象叫做逃逸,如果一个对象没有逃逸,则可以运用一些额外的优化技术,这种优化技术逃逸分析。通过逃逸分析” JIT可以使用如下技术优化:

 

栈上分配

消灭同步

消灭垃圾回收读写障碍

对象爆炸

-XX:+UseBiasedLocking,开启偏向锁,偏向锁是是锁偏爱上次使用它的线程,在非竞争锁的场景下,可以实现无锁的开销。

 

-XX:+UseLargePages,开启使用大页面,使用大页面可以提高TLB(translation lookaside buffer(转换后备缓存区))的缓存命中率;

 

-XX:PretenureSizeThreshold=n,指定对象占用的字节数超过n之后直接在老年代中分配,默认值0,表示最大值;

 

-XX:+OmitStackTraceInFastThrow,一些频繁抛出的异常,JVM为了性能优化而抛出没有堆栈的异常,默认开启,

 

-XX:+PrintFlagsInitial-XX:+PrintFlagsFinal,打印出设置的JVM参数和最终生效的JVM参数和他们的值,-XX:+PrintCommandLineFlags打印出被修改的JVM参数,一般在启动应用添加此参数打印出引用启动时添加的JVM参数,在应用运行过程中一些JVM参数可能会被修改掉(通过jinfo工具);

 

-XX:+PrintSafepointStatistics,打印安全点统计信息,-XX:PrintSafepointStatisticsCount=n设置打印安全点统计信息的次数;

-XX:+PrintReferenceGC,打印gc时处理的reference情况;

-XX:+UseCompressedOops,开启压缩指针,压缩指针主要是为了解决32位操作系统内存寻址范围只有4G的限制,开启压缩指针最大寻址范围增加到32G;如果内存超过32GXmx>32G),开启该参数无效;

-XX:+ParallelRefProcEnabled,使用此参数激活多线程方式的引用处理,截止到目前的jdk8该参数默认关闭,如果在gc中发现处理ref处理的时间过长,可以通过参数-XX:+PrintReferenceGC打印出每次垃圾收集中记录每个引用对象类型的统计数据,另外如果发现有大量软引用正在被处理,可以使用参数-XX:SoftRefLRUPolicyPerMB调整软件用的处理策略,该参数的默认值为1000ms),该参数的含义是,使用该参数设置的值乘以java堆可用空间(以兆为单位)的出来的值,若软引用在这段时间内没有被访问,那么这些软引用将会被回收,调优引用处理的目标主要有两点:第一是降低引用在gc过程中处理时间,第二是降低heap的占用空间,减少垃圾收集频率和最终需要复制的时间;

5.  参考文献

 

Java发展历史

https://blog.csdn.net/m0_51779342/article/details/116723871

 

Java特性

https://www.runoob.com/java/java-intro.html

 

Java内存模型

https://zhuanlan.zhihu.com/p/525293152

 

Java GC

https://blog.csdn.net/sinat_14913533/article/details/128301244

 

 

Java GC回收器说明

https://blog.csdn.net/weixin_42152237/article/details/119748198