命令行工具

jps

列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟唯一ID(与操作系统进程ID一致)。

jps [option] [hostid]

选项 作用
-q 只输出lvmid,省略主类的名称
-m 输出虚拟机启动时传递给主类main()的参数
-l 输出主类的全名,如果是jar包,则输出jar路径
-v 输出虚拟机启动时的jvm参数

jstat

用于监视虚拟机各种运行状态信息的命令行工具。
显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。

jstat [option vimid [interval[s|ms] [count]]]
[protocal:][//]lvmid[@hostname[:port]/servername]

选项 作用
-class 监视类加载、卸载数量、总空间以及类装载耗费的时间
-gc 监视Java堆状况,包括Eden、Survivor、老年代、永久代的容量,已用空间,垃圾收集时间
-gccapacity 监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到最大最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,会额外输出导致上一次垃圾收集产生的原因
-gcnew 监视新生代垃圾收集情况
-gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间
-gcold 监视老年代垃圾收集状况
-gcoldcapacity 与-gcold相同,输出主要关注使用到的最大、最小空间
-gcpermcapacity 输出永久代使用到的最大、最小空间
-compiler 输出即时编译器编译过的方法、耗时等信息
-printcompilation 输出已经被即时编译的方法

每隔1s查询一次2105的gc情况,查询10次。
jstat -gc 2105 1000 10

jstat -gcutil 2105

S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 6.20 41.42 47.20 16 0.105 3 0.472 0.577

jinfo

实时查看和调整虚拟机各项参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Usage:
jinfo [option] <pid>
(to connect to running process)
jinfo [option] <executable <core>
(to connect to a core file)
jinfo [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server)

where <option> is one of:
-flag <name> to print the value of the named VM flag
-flag [+|-]<name> to enable or disable the named VM flag
-flag <name>=<value> to set the named VM flag to the given value
-flags to print VM flags
-sysprops to print Java system properties
<no option> to print both of the above
-h | -help to print this help message

jmap

用于生成堆转储快照,查询finalize执行队列、Java堆和方法区的详细信息(空间使用率、当前使用收集器类型)

生成堆转储文件的方式:

1
2
3
4
-XX:+HeapDumpOnOutOfMemoryError 在虚拟机内存溢出异常出现后自动生成堆转储快照文件。
-XX:HeapDumpOnCtrlBreak 使用ctrl+Break 生成堆转储快照文件
kill -3 pid 生成堆转储快照文件
jmap -dump:format=b,file=heap.dump 3120
选项 作用
-dump 生成java堆转储快照,格式为-dump:[live,]format=b,file=,live子参数说明是否只dump出存活对象
-finalizerinfo 显示再F-Queue中等待Finalizer线程执行finalize方法的对象。只有在Linux平台有效
-heap 显示Java堆详细信息,使用回收器类型、参数配置、分代状况,只在Linux平台下有效
-histo 显示堆中对象统计信息,包括类、实例数量、合计容量
-permstat 在以ClassLoader为统计口径显示永久代内存状态。只在Linux下有效
-F 当虚拟机进程对-dump选项没有响应时,可以使用-F强制生成dump快照,只在Linux下有效

jhat

用于分析jmap生成的堆转储快照。

jhat heap.dump

jhat功能相对简陋,分析工作耗时且耗费硬件资源,一般不在本机使用。
替代方案:VisualVM、Eclipse Memory Analyzer、IBM HeapAnalyzer。

jstack

生成虚拟机当前时刻的线程快照。目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Usage:
jstack [-l] <pid>
(to connect to running process)
jstack -F [-m] [-l] <pid>
(to connect to a hung process)
jstack [-m] [-l] <executable> <core>
(to connect to a core file)
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server)

Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

高级工具

JConsole

基于JMX的可视化监视、管理工具。

JHSDB

基于服务性代理实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言实现的API集合。
JCMD 和JHSDB工具对比

VisualVM

多合一故障处理工具,不需要被监视的程序基于特殊Agent执行,通用性强,对应用程序的实际性能影响较小,可以直接在生产环境使用。

Java Mission Control

可持续在线监控工具。

arthas

阿里巴巴提供的性能分析工具
https://alibaba.github.io/arthas/

Centos 简单优化

安装基本工具
1
yum install -y wget vim net-tools lrzsz

删除UUID

1
2
3
vim ifcfg-ens33
vim ifcfg-eth0
vim em1

配置静态IP

1
2
3
4
5
IPADDR=10.0.0.130
GATEWAY=10.0.0.2
NETMASK=255.255.255.0
DNS1=10.0.0.2
DNS2=1.2.4.8

关闭防火墙以及NetworkManager和SELINX

1
2
3
4
5
systemctl restart network
systemctl stop firewalld
systemctl disable firewalld
systemctl stop NetworkManager
systemctl disable NetworkManager

替换YUM源

1
2
3
4
yum clean all && yum makecache
cd /etc/yum.repos.d/
mv CentOS-Base.repo CentOS-Base.repo.bak
vim CentOS-Base.repo

编译

  • 前端编译器: JDK的Javac 、EclipseJDT中的增量式编译器(ECJ)
  • 即时编译器:HotSpot虚拟机的C1\c2编译器、Graal编译器
  • 提前编译器:JDK的Jaotc、GNU Compiler for Java, Excelsisor JET.

前端编译器

编译过程分为1个准备过程和3个处理过程:
1)初始化插入式注解处理器
2)解析与填充符号表

  • 语法、词法分析
    词法分析是将源代码的字符流转变为标记集合的过程,单个字符是程序编写的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。

    语法分析时根据标记序列构造抽象语法树的过程,抽象语法树是用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表程序代码中的一个语法结构。例如包、类型、修饰符、运算符、接口、返回值、代码注释等。

    经过词法和语法分析生成语法树以后,编译器后续的操作都建立在抽象语法树之上。

  • 填充符号表
    符号表是一组符号地址和符号信息构成的数据结构,符号表中所登记的信息在编译的不同阶段都要被用到。如,在语义分析过程中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,对符号名进行地址分配时,符号表是地址解析分配的直接依据。

3)注解处理器
插入式注解处理器可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器(javac)的工作过程。

如果在处理注解期间对语法树进行过修改,编译器将回到解析以及填充符号表的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改位置,每次循环称为一个轮次。

4)语义分析与字节码生成

语义分析过程分为标注检查和数据以及控制流分析两个步骤。

在经过语法分析后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。而语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查等等。

  • 标注检查
    检查的内容包括变量是用钱是否已经被声明、变量与赋值之间的数据类型是否能够匹配等等。
    常量折叠:int a = 1 + 1 ; ==> int a = 2;

  • 数据流以及控制流分析
    数据流分析以及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检查异常都被正确处理了等问题。
    编译器的数据以及控制流分析与类加载时的数据以及控制流分析的目的基本哈桑一致,但是校验范围有所区别。

  • 解语法糖
    Java语言的语法糖包含泛型、变长参数、自动装箱拆箱等,Java虚拟机在运行时并不知节支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程称为解语法糖。

  • 字节码生成

    字节码生成阶段需要把前面哥哥步骤生成的信息转化成字节码指令写到磁盘中外,还要进行少量的代码添加和转换工作。
    如实例构造器<init>()方法和<cinit>()方法的添加。
    如果用户代码中没有提供任何构造函数,那么编译器将会添加一个没有参数的、可访问性与当前类型一致的默认构造函数,这个工作在填充符号表阶段已经完成。

    编译器会把语句块、变量变量初始化、调用父类的实例构造器方等操作收敛到<init>()和<cinit>()方法中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序执行,

语法糖

泛型、自动装箱、拆箱、遍历循环、变长参数和条件编译、内部类、枚举类、断言、数值直面量、枚举和字符串的switch、try with resource, lambda等。

泛型

类型擦除

自动装箱、拆箱和循环遍历

1)大于127的值不会自动拆箱
2)没有遇到运算符的情况下不会自动拆箱
3)equals方法不处理数据转型

变长参数

条件编译

Java编译器将所有编译但愿的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间可以互相提供符号信息。

根据条件中布尔值常量值的真假,编译器将把分支中不成立的代码块消除掉。

后端编译

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存。
当程序启动以后,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,减少解释器的中间损耗,获得更高的效率。

即时编译器

即时编译器将运行比较频繁的代码块或者方法编译成本地机器吗,并且对这段代码进行优化。

在Hotspot中内置了2个即时编译器 Client Compiler、 Server Compiler。 虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用-client 或者 -Server 来强制指定虚拟机运行在客户端模式或者服务端模式。

解释模式(Interperted Mode):-Xint
编译模式(Compiled Mode): -Xcomp
混合模式(Mixed Mode): -Xmixed (默认)

编译器编译本地代码需要占用程序运行时间,位了打到响应速度与运行效率之间打到最佳平衡,HotSpot在编译子系统中加入了分层编译的功能。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次:
第0层,程序春解释执行,并且解释器不开启性能监控。
第一层:使用客户端编译器将字节码编译为本地代码,进行简单可靠的稳定优化,不开启性能监控功能。
第二层:使用客户端编译器执行,仅开启方法以及回边次数统计等优先的性能监控功能。
第三层:使用客户端编译器执行,开启全部性能监控,除了第2层的痛就之外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
第四层:使用服务端编译器将字节码编译为本地代码,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

实施分层编译后, 解释器、客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多次编译,用客户端编译器获得更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无需额外承担手机性能监控信息的任务,而在服务端编译器采用高复杂度的优化算法时, 客户端编译器可先采用简单优化来为它争取更多的编译时间。

编译对象与触发条件

被编译的热点代码有:被多次调用的方法和被多次执行的循环体。这两种情况编译的目标对象都是整个方法体,而不是单独的循环体。

基于采样的热点探测

周期性的检查线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法。
优点:简单高效,容易获取方法的调用关系
缺点:不能精确确认方法的忍睹,容易受到线程阻塞或者外界因素影响扰乱热点探测。

基于计数器的热点探测

为每个方法建立计数器,统计方法的执行次数,如果超过一定的阈值就认为它是热点方法。
优点:结果精确严谨
缺点:每个方法建立并维护计数器,不能直接获取到方法的调用关系。
HotSpot为每个方法准备了两类计数器:方法调用计数器和回边计数器(在循环边界往回跳),当调用次数超过设定的阈值,就会触发即时编译。
-XX:CompileThreshold 来设定触发即时编译的阈值。
默认设置下,方法调用计数器统计的是相对执行瓶绿,当一个时间段内调用次数不足以提交即时编时,这个方法的调用计数器就会被减半(衰减),可以使用-XX:-UseCounterDecay关闭热度衰减。可以设置-XX:CounterHalfLifeTime设置半衰周期的时间,单位是秒。

当方法被调用时,首先检查该方法是否被即使编译器编译过,是则优先使用编译后的版本执行,否则将方法调用的计数器加一,然后判断方法调用计数器与回边计数器值之和是否超过阈值,超过则像即时编译器提交该方法的代码编译请求。
如果没有做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成。当编译工作完成后,这个方法的调用入口地址会被系统自动改写成新值,下次调用该方法时就会使用已编译版本。

方法调用计数器 触发即时编译

回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”,回边计数器的目的是为了触发栈上的替换编译。回边计数器没有热度衰减过程。

编译过程

一般情况下,方法调用产生的标准编译请求和栈上替换编译请求,在虚拟机编译器未完成编译之前,都将按照解释方式执行代码,编译动作在后台的编译线程完成。
设置-XX:-BackgroundCompilation禁止后台编译,禁止后触发即时编译条件时,执行线程向虚拟机提交编译请求后一致阻塞等待,直到编译过程完成后再开始执行编译器输出的本地代码。

服务端编译器效率较低,但是编译速度远超传统的静态优化编译器,相对于客户端编译器编译输出的代码质量有很大的提交,可以大幅降低本地代码的执行时间,从而抵消掉额外的编译水间开销。

即时编译的缺点

1)占用程序运行时间
2)占用程序运算资源

提前编译器

提前编译器的分支:
与传统C、C++编译器类似,在程序运行之前把程序代码编译成机器码的静态翻译工作;
把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码时直接加载进来使用。

编译器优化技术

方法内联

最重要的优化技术之一。

逃逸分析

最前沿的优化技术之一。

  • 栈上分配
    支持方法逃逸,但不支持线程逃逸

  • 标量替换
    如果一个数据已经无法在分解为更小的数据表示时(原始数据类型),这些数据就可以称为标量。
    标量替换就是将用到的成员变量恢复为原始类型来访问的过程。
    加入逃逸分析可以证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候可能不去创建这个对象,而改为直接创建它的若干个被这个的方法使用的成员变量来替代,成员变量存储在栈上。

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

公共子表达式消除

语言无关的经典优化技术。如果一个表达式E之前已经被计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。

数组边界检查消除

语言相关的经典优化技术。

Java技术体系的自动内存管理最根本目标是自动化地给对象分配内存以及自动回收分配给对象的内存。
《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪种垃圾收集器,以及虚拟机中与内存相关的参数设定。

HotSpot虚拟机中对象的分配原则

对象优先在Eden分配

在大多数情况下,对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时,虚拟机将发起一次minorGC。
当survivor区的空间无法存放Eden区的对象时,就会通过担保机制提前转移到老年代去。

-Xms:20M 堆内存初始值20M
-Xmx:20M 堆内存最大20M
-Xmn10M 新生代占用10M(剩下的10M留给老年代)
-XX:SurvivorRatio=8 决定了新生代Eden区与一个Survivor区的空间比例是8:1:1

大对象直接进入老年代

大对象是需要大量连续内存空间的Java对象,如很长的字符串或者元素数量很多的数组。

在分配空间时,如果对象比较大,就会提前触发垃圾收集,以获取足够的连续空间才能存放该对象, 当复制对象时大对象意味着高额的内存复制开销。

-XX:PretenureSizeThreshold=3145728 (大于3M的对象会直接分配到老年代中)
该参数只对Serial和ParNew两款收集器有效。

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

内存回收时必须决策那些存活对象应该存放在新生代,哪些存活对象存放在老年代。
虚拟机为每个对象定义了一个对象年龄(Age)存储在对象头重。

对象通常在Eden区诞生,如果经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,会被移动到Survivor空间,并且将对象的年龄设置为1岁。对象在Survivor中每经过一次MinorGC,Age增加1, 当年龄增加到一定程度的时候,该对象会被移动到老年代中。
可以通过-XX:MaxTenuringThreshold设置(默认为15次)。

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。

也就是当前Survivor空间大小为1M,有两个Age=1且大小为256k的对象时,这两个对象就会直接进入老年代。

空间分配担保

在发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果条件成立,那么这次MinorGC时安全的;;
如果不成立,虚拟机会先查看-XX:HandlerPromotionFailure参数的设置值是否允许担保失败
如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次今生到老年代对象的平均大小
大于,尝试进行一次MinorGC;
小于,或者-XX:HandlePromotionFailure不允许冒险,则变更为FullGC。

在JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小,就会进行MinorGC,否则就进行FullGc。-XX:HandlePromotionFailure参数不在生效。

垃圾的回收

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

如何判断对象已死

  • 引用计数器
  • 可达性分析算法(固定可作为GCRoot的对象有)
    • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如每个线程背调用的方法堆栈中使用的参数、局部变量、临时变量
    • 在方法区中类静态属性引用的对象,如Java类的引用类型静态变量
    • 在方法区中常量引用的对象,如字符串常量池中的引用
    • 在本地方法栈中JNI引用的对象
    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
    • 所有背同步锁持有的对象
    • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

对象的引用

  • 强引用,如Object obj = new Object(); 无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉背引用的对象。
  • 软引用,描述有用但非必须的对象,被软引用关联的对象在系统将要发生内存溢出前会把这些对象例如回收范围内进行二次回收。(SoftReference)
  • 弱引用,描述非必须对象,被弱引用的对象只能生存到下一次垃圾收集发生位置。无论内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用,一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的时为了能在这个对象被收集器回收时受到一个系统通知(NIO用的RdirectMemory)

垃圾收集器算法

分代收集理论

建立在两个分代假说上:
1)弱分代假说,绝大数对象都是朝生夕灭的。
2)强分代假说,经过越多次收集过程的对象月难消亡。
3)(推理假说)跨代引用假说,跨代引用相对于同代来说仅占极少数。

分代收集区域
  • 部分收集(Partial GC)
    • 新生代收集(Minor GC/ Yong GC)
    • 老年代收集(Major GC / Old GC)(只有CMS会单独回收老年代)
    • 混合收集(Mixed GC),目前只有GC存在混合回收行为。
  • 整堆收集(Full GC)

标记清除算法

标记需要被回收的对象后,回收掉所有被标记的对象;
或者标记不需要被回收的对象,同一回收未被标记的对象。

缺点:
不稳定,标记清除的性能随着对象的增多而降低;
空间碎片化问题,肯能会导致在分配较大对象时无法找到足够的连续空间内存不得不提前触发另一次垃圾收集动作。

mark-sweep

标记复制算法

为了解决标记清除算法面对大量可回收对象时执行效率低的问题。

优点:只需要复制少数存活对象,简单高效
缺点:浪费内存空间。

mark-copy

根据弱分代假说,以及实际情况,大部分的对象在第一次垃圾回收时就会被回收,因此并不需要按照1:1的比例来划分新生代的内存空间。
内存被分为较大的Eden区和较小的两个Survivor区域,HotSpot默认比例是8:1:1。
打给你Survivor空间不足容纳一次MinorGC后存活的对戏那个时,就需要依赖其他内存区域进行分配担保。

标记整理算法

根据老年代对象的存亡特征以及强分代假说提出的算法。
将所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。

mark-compact

吞吐量:赋值器和收集器的效率总和。
如果在清理时移动,必然会影响到对象回收的效率。
如果清理时不移动,必然会影响到对象分配的效率。

常见的垃圾收集器

Serial / SerialOld

Serial收集器是最古老的收集器,使用标记拷贝算法,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:在内存较小的桌面应用或者Client模式下,额外内存消耗最小,简单高效。

SerialOld同样是单线程收集起,使用标记整理算法。当CMS收集起发生失败时会转为SerialOld工作。

Serial GC

PS / PO

Paralle Scavenge/ Paralle Old 的目标是达到一个可控的吞吐量。

ParalleScavenge 同样是基于标记-复制算法,支持多线程并发收集,
吞吐量 = (运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间)
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
-XX:GCTimeRatio 控制垃圾收集时间占总时间的比率
-XX:+UseAdaptiveSizePolicy 就不需要人工指定新生代的大小,Eden于Survivor的比例,以及晋升老年代的大小,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

ParalleOld 收集老年代,基于标记整理算法。

Paralle Scavenge / Paralle Old

ParNew / CMS

ParNew是Serial收集器的多线程并行版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集起完全一致。

ParNew / SerialOld

CMD是一种以获取最短回收停顿时间为目的的收集器。符合希望系统停顿时间尽可能短,以给用户带来良好交互体验的需求。他是一种标记-清除算法实现的。
包含四个步骤:
1)初始标记
2)并发标记
3)重新标记
4)并发清除

其中初始标记和重新标记这两个步骤仍需要STW。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象、速度很快;
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长,但不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
重新标记为了修正并发标记期间因为用户程序继续运行导致标记产生变动的部分,这个阶段比初始标记耗时稍长;
并发清除,清理删除掉标记阶段判断已经死亡的对象,不需要移动哦存活对象,因此这个阶段也是与用户线程同时并发。

CMS

CMS缺点:
1) 耗CPU资源
2) 无法处理浮动垃圾,由于预留的内存无法分配新对象时,有可能出现回收失败而导致退化到Serial Old的模式收集。
3)分配占用大空间的对象时,会出现老年代还有很多剩余空间,但是无法找到足够大的连续空间分配对象,不得不提前触发FullGC。
提供了两个参数优化,但是JDK1.9之后已经废弃:
-XX:UseCMS-CompactAtFullCollection 不得不进行FullGC时开启内存碎片的合并整理。
-XX:CMSFullGCsBefore-Compaction,要求CMS收集器在执行过若干次不整理空间的FullGC之后,下一次进入FullGC前进行碎片整理。

Garbage First

G1时一款主要面向服务端应用的垃圾收集器。

在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

G1面向堆内存任何部分来组成回收集(CollectionSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的MixedGC模式。

G1把连续的Java堆内存划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演 新生代的Eden空间Survivor空间,或者老年代空间。收集器能够对不同角色的Region采用不同的=策略取处理,这样无论是新创建的对象还是存活了一段时间、经过多次收集的旧对象都能获得很好的收集再熬过。

Region中有一类Humongous区域专门存放大对象。 G1认为只要大小超过了一个Region容量的一般的对象就可以判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围1MB~32MB,且应为2的N次幂。那些超过了整个Region容量的超级大对象,会被存放在N个连续的HumongousRegion之中,G1的大多数行为都把HumongousRegion作为老年代的一部分进行看待。

G因为将Region作为单词回收的最小单元,所以可以建立可预测的停顿时间模型。
每次根据用户设置的停顿时间-XX:MaxGCPauseMillis, 优先处理回收价值收益最大的那些Region。
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在悠闲的时间内获取尽可能高的收集效率。

G1 Region

需要解决的问题

1)跨Region引用的对象: 每个Region都维护自己的记忆集避免全堆作为GCRoots扫描, 这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
G1的记忆集是一种Hash表,Key时别的Region的起始地址,Value时一个集合,里面存储的元素时卡表的索引号。这种双向的卡表结构记录了我指向谁和谁指向了我,因此G1只少需要耗费大约10%~20%的额外内存来维持收集工作。

2)在并发阶段如何保证收集线程与用户线程互不干扰地运行?
用户线程在改变对象引用关系的时候,必须保证不能打破原本的对象图结构,导致比阿吉结果出现错误,
CMS使用增量更新算法,G1使用原始快照算法来实现。

垃圾收集对用户线程的影响还体现在回收过程中心窗卷对象的内存分配上,程序要继续运行就肯定会持续有新对象呗创建,G1位每个Region设计了两个名为TAMS(TOp at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时心分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象时被隐式标记过的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致FullGC而产生长时间的STW。

3)如何建立可靠的停顿模型
G1的停顿预测模型时以衰减均值为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可以测量的步骤话费的成本,并分析得出平均值,标准偏差、可行度等统计信息,衰减平均值时比普通的平均值更容易受到新数据的影响,平均值更容易受到新数据的影响,平均值代表整体平均状态,衰减均值代表最近的平均状态。 Region的统计状态越新越能决定器回收的价值。通过这些信息预测现在开始回收的话,哪些region组成回收集采可以在不超过期望停顿时间的约束下获得最高的收益。

G1的回收步骤

初始标记

只标记一下GCRoots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段的用户线程并发运行时,能正确滴在可用的Region中分配新对象。这个阶段需要停顿线程,但是耗时短,而且是借用进行MinorGC的时候同步完成,所以G1收集器在这个阶段实际并没有额外的停顿。

并发标记

从GCRoots开始对对子红对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但是可以和用户程序并发执行,当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

最终标记

对用户线程做另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。

筛选回收

负责更新Region的统计数据,对哥哥Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间,对存活对象移动时必须暂停用户线程,但是由多条线程完成的。

G1

G1从整体上来看是基于标记整理算法,但从局部来看时基于标记复制算法。无论如何,这两种算法意味着G1再与性期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发限一次收集。

G1位了垃圾收集产生的内存占用以及额外的执行副在都比CMS搞。
内存占用是由于双向卡表占用空间大。CMs只记录了老年代到新生代的卡表记录。

负载:CMS用写后屏障维护更新卡表;
G1除了使用写后屏障维护卡表外,位了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发前的指针变化。 原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点。

CMS的写屏障是同步操作,G1使用的是消息队列,把写前屏障和写后屏障中要做的事情都放在队列里,再异步处理。

目前线程是java中进行处理器资源调度的最基本单位。(Loom项目正在尝试改变线程这一重量级的实现)

实现线程的方式主要有三种:

  1. 使用内核线程1:1实现;
    内核系统就是由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

内核线程

  1. 使用用户线程1:n实现;

用户线程

  1. 使用用户线程价轻量级进程混合实现(n:M实现)

混合实现

Java线程的实现

《Java虚拟机规范》中没有限定使用哪种线程模型来实现,主流平台上的主流商用Java虚拟机线程模型普遍都给予操作系统原生线程模型来实现(1:1的线程模型)

Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,调度方式分为两种:

  • 协同式线程调度
    线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,同志系统切换到另外一个线程。
  • 抢占式线程调度
    每个线程由系统来分配执行时间,线程的切换不由线程本身来决定。
    Java可以使用thread.yield()方法主动让出执行时间,但是并不能主动获取执行时间。

线程的优先级

我们并不能在程序中通过优先级来完全准确判断一组状态都为Ready的线程先执行哪一个?

状态转换

Java语言定义了6中线程状态,在任意一个时间点中, 一个线程只能有且只有一种状态,并且可以通过特定的方法在不同状态之间转换。

  • 新建:new,创建后尚未启动的线程处于新建状态;
  • 运行:runnable,包括操作系统线程状态中的Running和Ready,
  • 等待:Waiting,处于这种状态的线程不会被分配处理器执行时间,它们需要被其他线程显示唤醒。
    以下方法可以让线程无限期等待:
    • 没有设置timeout参数的Object.wait()方法;
    • 没有设置timeout参数的Thread.join()方法;
    • LockSupport.part()方法;
  • 限期等待:处于这种状态的线程也不会被分配处理器执行时间,在一定时间后会有系统自动唤醒。
    • Thread.sleep()
    • 设置了Timeout的Object.wait()
    • 设置了Timeout的Thread.join()
    • LockSupport.parkNanos();
    • LockSupport.parkUntil();
  • 阻塞:Blocked,在程序等待进入同步区域的时候,线程将进入阻塞状态。
  • 结束: Terminated,已终止线程的线程状态,线程已经结束执行。

线程状态图

线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要执行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

也就是代码本身封装了所有必要的正确性保障手段,令调用者无序关心多线程下的调用问题,更无需自己实现任何措施来保证多线程环境下的正确调用。

不可变

在Java语言中,不可变对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。

只要一个不可变的对象被正确的构建出来,那其外部的可见状态永远都不会改变,永远都不会看到他在多个线程之中处于不一致的状态。

绝对线程安全

绝对的线程安全除了对象本身外, 还需要在调用端做相应的同步措施。

相对线程安全

保证这个对象在单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。在Java中,相对线程安全的类型有Vector、HashTable、Collections.synchronizedCollection();

线程兼容

对象本身并不是线程安全的, 但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全使用。
Vector、HashTable、ArrayList、HashMap等都是线程兼容的。

线程对立

如果两个线程同时持有一个线程对象,一个尝试去中断线程、一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在思索的风险。(Thread类的suspend和resume方法),常见的对立操作还有System.in() System.out()和System.runFinalizersOnExit();

线程安全的实现方法

互斥同步

用互斥来实现同步。
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。
互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式。
因此它面临的问题是进行线程阻塞和唤醒所带来的性能开销,因此也叫阻塞同步。
属于一种悲观的并发策略。

synchronized(非公平锁)
java.util.concurrent.locks.Lock
ReentrantLock(非公平锁)

可重入锁比synchronized增加了一些高级功能:

  • 等待可中断
    当持有锁的线程长期不释放所得时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 公平锁
    多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序依次获得锁。
  • 锁绑定多个条件
    一个ReentrantLock对象可以同时绑定多个Condition对象, 多次调用newCondition()即可。

非阻塞同步

基于硬件冲突检测的乐观并发策略,也就是说先进性操作,如果没有其他线程争用共享数据,操作成功;
如果共享数据被争用,产生冲突,那么再进行其他的补偿措施,最常用的补偿措施就是不断重试,直到成功。
这种方式不需要阻塞线程,因此也叫无锁编程。
常用的具有原子性的硬件操作和冲突检测指令有:

  • 测试并设置(Test-And-Set)
  • 获取并增加(Fetch-And-Increment)
  • 交换(Swap)
  • 比较并交换(Compare And Swap)
  • 加载链接/条件存储(Load-Linked/ Store-Conditional)

Java中暴露出来的是CAS指令,当CAS指令执行时,当且仅当内存地址V的值符合预期值A时,处理器才会用新的值B更新内存地址V的值,该过程是一个原子操作,执行期间不会被其他线程中断。

CAS存在的ABA问题,大部分情况下不会影响程序并发的正确性,如果需要解决该问题,传统的互斥同步可能会比原子类更为高效。

无同步方案

如果可以让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的。

  • 可重入代码
    如果一个方法的返回结果是可预测的,只要输入了相同的数据,就就能返回相同的结果,那么它就满足可重入性的要求,当然他就是线程安全的。
    可重入代码不依赖于全局变量、存储在队上的数据和公用的系统资源,用到的状态量都由参数重传入,比调用非可重入的方法等。
  • 线程本地存储
    如果一段代码中使用的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。这样就可以把共享数据的可见范围限制在一个线程之内。
    常见的有生产者消费者模式(Web交互模型)。
    ThreadLocal的使用。

锁优化

自旋锁和自适应自旋

互斥同步的时候需要对线程进行挂起和恢复,但是挂起和恢复需要陷入内核态来完成。
因此当一个线程获取锁的时候,如果暂时没有获取到,那么让他稍微等一会儿,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。

如果锁被占用很长的时间,那么自旋的线程只会白白消耗处理器资源。因此自旋等待时间必须有一定的限度。
如果自旋超过了限定的次数,仍然没有成功获得锁,就应当使用传统方式去挂起线程。

自旋锁的实现

几种自旋锁的java实现
认真的讲一讲:自旋锁到底是什么
看完你就明白的锁系列之自旋锁

锁消除

虚拟机即使编译器在运行时,判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当作栈上的数据对待,认为他们是线程私有的,消除同步锁的过程。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的, 可以把加锁同步的范围扩展到整个操作序列的外部。

轻量级锁

在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

Hotspot对象头分为两部分:
1)用于存储对象自身运行的数据,如哈希吗,GC分代年龄。这部分数据的长度在32位和64位的Java虚拟机中分别占用32个或者64比特。这部分是实现轻量级锁和偏向锁的关键。
2)用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分存储数组长度。

由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,MarkWord被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。
它会根据对象的状态服用自己的存储空间。
在32位的Hotspot虚拟机中,对象未被锁定的状态下,MarkWord的32个比特空间有25个比特用于存储对象的哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有1个比特股定位0(表示未进入偏向模式)。对象除了违背锁定的正常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种状态。

如果代码即将进入同步块的时候,如果此同步对象没有被锁定(标志位位01),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(LockRecord)的空间,用于存储锁对象目前的MarkWord的拷贝。

然后虚拟机将使用CAS操作尝试把对象的markword更新为指向LockRecord的指针。如果这个更新成功,那么代表该线程拥有了这个对象的锁,并且对像MarkWord的锁标志位将转变为00,表示此对象处于轻量级锁定状态。

如果更新操作失败,意味着至少存在一个线程与当前线程竞争获取该对象的锁。虚拟机首先检查对象的MarkWord是否只想当前线程的栈帧,如果是,说明线程已经拥有了这个对象的锁,那么直接进入同步块继续执行,否则说明这个对象已经被其他线程抢占了。
如果出现两条以上的线程争用同一个锁的情况,那么轻量级锁不再有效,必须要膨胀为重量级锁,锁标志的状态变为10, 此时Markword中存储的就是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。

它的解锁过程也是通过CAS来进行的,如果对象的MarkWord仍然指向线程的锁记录,那就用CAS操作把对象当前的MarkWord和线程中复制的DisplacedMarkWord替换回来,如果替换成功,说明解锁成功。
替换失败,说明有其他线程尝试过获取该锁,需要在释放锁的同时唤醒被挂起的线程。

如果没有竞争,轻量级锁能通过CAS操作成功避免使用互斥量的开销;
如果存在竞争,除了互斥量本身开销外,还额外发生了CAS操作的开销,因此性能更差。

偏向锁

用于消除数据在无竞争情况下的同步,连CAS都去掉了,进一步提高程序的运行性能。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一致没有被其他的线程获取,则持有偏向锁的线程永远不需要再同步。

如果当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为01,把偏向模式设置为1,表示进入偏向模式。
同时使用CAS操作把获取到这个锁的线程ID记录在对象的MarkWord中。
如果CAS成功,持有偏向锁的线程以后每次进入这个锁相关的同步块是,虚拟机都可以不再进行任何同步操作。

一旦出现另外一个线程尝试获取这个锁的情况,偏向模式马上宣告结束。
根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(把偏向模式设置为0),撤销后标志位恢复到为锁定或者轻量级锁定的轧辊台,后续的同步操作按照轻量级锁执行。

偏向锁

当进入偏向状态是,MarkWord大部分的空间都用于存储持有锁的线程ID,这部分空间占用了原有存储对象Hash码的位置。当一个对象已经计算过一次一致性哈希码后,它就再也无法进入偏向锁状态。
当一个对象正在处于偏向锁状态时,受到计算hash码的请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。

在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类中有字段可以记录非加锁状态下的MarkWork,其中自然可以存储原来的哈希码。

偏向锁可以提高带有同步但无竞争的程序性能,但如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。

当代码被编译成二进制字节码,由Classloader加载到内存,经过加载、验证、准备、解释后将数据转化为JVM可以接受的格式存储到方法区,并在堆内存中创建一个Class对象,作为外部访问的入口。
接下来就由字节码执行引擎来解释执行代码。

《Java》虚拟机规范中规定了Java虚拟机字节码执行引擎的概念模型,输入字节码二进制流,输出执行结果。

在不同的虚拟机实现中,执行引擎在执行字节码的时候会有两种:

  • 解释执行(Sun Classic 虚拟机)
  • 编译执行(Jrockit)
  • 两种兼备 (HotSpot)

运行时栈帧结构

栈帧是用于支持虚拟机方法进行调用和执行的数据结构,每个方法从调用开始到执行结束的过程,都对应一个栈帧从虚拟机栈中入栈到出栈的过程。

栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

栈帧

栈帧中需要多大的局部变量表、多深的操作数栈在编译Java的时候就已经计算出来存放在方法表的Code属性中。

局部变量表

用于存放方法参数和方法内部定义的局部变量。

在代码被编译成Class文件时,在方法的Code属性中max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。局部变量表的容量以变量槽(Variable Slot)为最小单位,每个变量槽应该可以存放一个boolean、byte、char、short、int、float、reference、returnAddress这8种不超过32位数据类型(对于超过32位的long和double,则使用两个32位的变量槽)。

引用类型:

  • 通过引用可以找到这个对象在堆中的数据存放的起始地址或者索引;
  • 找到对象所属类型在方法区中的存储类型;

returnAddress是为字节码指令jsr、jsr_w 和ret服务,指向了一条字节码指令的地址,一些jvm使用这几条指令来实现异常处理时的跳转。
现在全部改为采用异常表来代替。

当一个方法被调用的时候,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,如果是实例方法局部变量表第0位索引的变量槽存放的是this。

以恰当的变量作用域来控制变量的回收时间才是最优雅的解决方法,而不是使用完后就将其引用赋予null, 因为这仅仅是基于虚拟机的概念模型层面作出的优化,虚拟机的具体实现不同,解释执行与编译执行结果都不同。

操作数栈

一个后入先出的栈,同局部变量表一样,操作数栈的最大深度也在编译的时候就被写入到Code属性的max_stacks数据项中。32位数据类型所占用的栈容量是1, 64位数据类型所占的栈容量是2.

通过将操作数入栈出栈来进行算术运算;
通过操作数栈将方法的参数进行传递;

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用时为了支持方法调用过程中的动态连接。

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令以常量池指向方法的符号引用作为参数。

这些符号引用一部分在类加载阶段或者第一次使用会被转化为直接引用;
另一部分在每次运行期间都转化为直接引用,这部分称为动态连接;

方法返回地址

当一个方法退出以后,必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,来帮助恢复主调方法的执行状态。

一个方法的退出有两种方式:
第一种是执行引擎遇到方法返回的字节码指令;主调方法的PC计数器的值就可以作为返回地址。
另一种是遇到了异常,并且在本方法的异常表中没有搜索到匹配的异常处理器导致方法退出;

方法调用

方法调用阶段的任务是确认被调用方法的版本。
Class文件的编译过程不包含传统程序语言的连接步骤,一切方法调用在Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址。因此一些调用需要在加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

在类加载的解析阶段,将静态方法和私有方法对应的符号引用转化为直接引用。因为静态方法与类关联,私有方法不可能被继承或者重写,在编译阶段即可确定。

字节码指令集设计了不同的指令调用不同类型的方法:

  • invokestatic, 调用静态方法
  • invokespecial, 调用实例构造器方法,私有方法,父类中的方法。
  • invokevirtual, 用于调用所有的虚方法(被final修饰的方法)
  • invokeinterface,用于调用接口方法,会在运行时再确定一个实现接口的对象
  • invokedynamic, 在运行时动态解析出调用点限定符引用的方法。

前四条指令,分派逻辑都固化在Java虚拟机内部,invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的。

只要能被invokestatic 和 invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本。Java语言中符合这个条件的方法有静态方法、构造方法、私有方法、父类方法和被final修饰的方法,这五种方法被称为非虚方法,其他的被称为虚方法。

解析调用是一个静态的过程, 在类加载的解析阶段就把涉及的符号引用转换为明确的直接引用,不必延迟到运行期再去完成。

分派

  • 静态分派
    所有依赖静态类型来决定方法执行版本的分派叫做静态分派。静态分派的典型应用表现是方法重载。
    方法重载发生在同一个类中,方法名称相同,参数列表不同。

  • 动态分派与实现
    在运行期根据时机类型确定方法执行版本的分派过程叫动态分派,它就是Java语言中方法重写的本质。多态的根源在于虚方法调用指令invokevirtual的执行逻辑,因此多态只对方法有效,对字段无效(字段永远不参与多态)。

    动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接受着类型方法元数据中搜索合适的目标方法,

  • 单分派和多分派
    方法的接收者(要被执行的方法的所有者叫接收者)与参数统称为方法的宗量。
    单分派是根据一个宗量对目标方法进行选择;
    多分派是根据多个宗量对目标方法进行选择;

Java中的解释执行与编译执行

编译过程

在执行前先对程序源码进行词法分析和语法分析处理,把源码转化为抽象语法树。

对于C/C++来说,词法、语法分析以后到后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整有意义的编译器去实现,这类就是编译执行。

对于Java来说,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再便利语法树生成线性的字节码指令流的过程。

基于栈的指令集与给予寄存器的指令集

  • 基于栈的指令集
    零地址指令,依赖操作数栈进行工作,
    优点是可移植性高,不与具体的硬件绑定。编译器实现简单。
    缺点是速度慢。
  • 给予寄存器的指令集
    而地址指令集,物理硬件直接支持的指令集架构,依赖寄存器工作。

虚拟机最终会对执行过程作出一系列优化来提高性能, 实际的情况会和盖面模型差距非常大,差距产生的根本原因是虚拟机中解析起和即时编译起都会对输入的字节码进行优化,即使解释其中也不是按照字节码指令去逐条执行的。

培养良好的生活、学习、工作习惯。

相关概念:

  • Etcd
  • coreOS
  • RancherOS

主机规划:

要求:主机名称规范、IP段相邻、备注明确

主机序号 名称 内网IP 外网IP CPU核数 内存大小 备注
01 etcd-node-master-01 10.0.0.1 外网IP 2 4G 备注
02 etcd-node-slave-02 10.0.0.2 外网IP 2 4G 备注
03 etcd-node-slave-03 10.0.0.3 外网IP 2 4G 备注

设置主机名称

1
2
3
hostnamectl set-hostname etcd-node-master-01
hostnamectl set-hostname etcd-node-slave-02
hostnamectl set-hostname etcd-node-slave-03

修改内网IP地址:

1
vim /etc/sysconfig/network-scripts/ifcfg-ens33

重启网关:

1
systemctl restart network

防火墙设置:

1
2
firewall-cmd --zone=public --add-port=2379/tcp --permanent
firewall-cmd --reload

YUM安装:

1
yum install -y etcd

验证&查看版本

1
2
rpm -qa etcd
etcdctl -v

修改配置:

分别修改3个主机的配置文件/etc/etcd/etcd.conf

注意!修改前一定要备份!
cp /etc/etcd/etcd.conf /etc/etcd/etcd.conf.bak

1
2
3
4
5
6
7
8
ETCD_LISTEN_PEER_URLS="http://10.0.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.0.0.1:2379,http://127.0.0.1:2379"
ETCD_NAME=Master
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.0.0.1:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.0.0.1:2379"
ETCD_INITIAL_CLUSTER=“Master=http://10.0.0.1:2380,Node02=http://10.0.0.2:2380,Node03=http://10.0.0.3:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_INITIAL_CLUSTER_STATE="new"

重启etcd服务

1
systemctl restart etcd

查看Etcd集群

1
etcdctl member list

Etcd 测试

1
2
etcdctl set name test
etcdctl get name
扩展概念
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
负载均衡:
区域负载均衡:DNS
硬件负载均衡:F5/A10
软件负载均衡:Nginx/LVS/HAProxy

磁盘阵列:raid
防火墙技术:硬件/软件

NTP(Network Time Protocal)是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化tongbuwang。

Radis的分布式缓存;
mysql主从、读写分离、高可用、分库分表;

MHA
备份
elk
ci/cd
centos8 / podman