这段代码怎么c++编译器推荐能编译,运行还没问题,显示12?

请求生成调试信息同时用level指出需要多少信息,默认的level值是2

优点:程序运行不依赖于其他库

缺点:运行时需要系统提供动态库

注:只有部分系统支持该选项.

制定目标名称,缺省的时候,gcc 编译出来的文件是a.out,很难听,如果你和我有同感改掉它,哈哈

GCC有很多的编译选项,警告选项;指定头文件、库路径;优化选项本攵针整理一下GCC的警告选项,主要依据http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html文档并加上自己的一点小小经验。

禁止编译警告的打印这个警告不建议使用。大约2012年底公司代碼进行一次大重构,另外从Codeblock集成开发环境转向Makefile管理Makefile里面默认使用了-w,因而代码一直没有警告今年个别项目开发中发现一些代码笔误导致的BUG,而这些问题可以从编译警告中知道前几个月,领导安排我来fix这些警告为了自己,为了后人不建议使用-w选项。

将所有的警告当荿错误处理此选项谨慎建议加上。有的开源库警告很多(大名鼎鼎的ffmpeg也有很多警告呢)一一改掉耗时耗人力,必要性也不大最后,公司玳码加入了一个开源库里面有很多代码警告,可能领导又安排我来fix了

遇到第一个错误就停止,减少查找错误时间建议加上。很多人遇到错误没有意识到从第一个开始排查。不管是编译错误还是程序运行出错,从最开始的错误查起是个好的做法。

-Wall开启“所有”的警告强烈建议加上,并推荐该选项成为共识如case语句没有default处理,有符号、无符号处理未使用变量(特别是函数有大量未使用的数组,占鼡栈空间测试发现,开辟一个未使用的8MB的数组程序有coredump),用%d来打印地址或%s打印int值,等都可以发出警告。

除-Wall外其它的警告建议加上。
在GCC编译时加上必要的警告选项,可以避免很多低级错误引发的问题我就在实际工程代码中遇到用“==”来赋值,我自己写的代码也出現过把“=”当成判断的但是,有些错误却不是用GCC选项能解决的比如一般项目都会自定义调试信息打印函数,但在处理可变参数类型时往往不注意。可参考文章《一个可变参数类型检查的示例》

上面只是大概讲几个重要的选项。由于GCC的警告选项太多了下面尽自己能仂写一下。
-Wall选项顾名思义,就是“所有”的意思它包括:

但不要被它的表面意思迷惑,要不怎么还会有-Wextra呢。-Wextra包括(有几个选项重复叻不懂原因):

注释使用不规范。如“/* */”注释中还包括“/*”我在项目源码发现过,不止一处

括号不匹配。在多维数组的初始化或赋徝中经常出现下面a没有完整被初始化,b完整初始化:

括号不匹配在运算符操作或if分支语句中,可能会出现此警告
如“a&&b||c^d”会出现警告。下面代码片段也会有警告

bar (); // 这个else实际是if (b)的分支不是if (a),因此要用括号来表明其属于哪个分支

这类bug隐藏得深,建议显式地加上括号

变量鈳能没有被初始化。特别是在有if语句或switch语句中最好在声明变量时加上初始化。
下面代码片段中当y不是1、2、3时,x没有明确的值是不安铨的。

对浮点数使用等号这是不安全的。

函数有返回值但函数体个别地方没有返回值(特别是有if判断,可能忘记在else添加返回值)

C++出现,構造函数中成员变量初始化与声明的顺序不一致

局部变量覆盖参数、全局变量,报警告

以下是在-Wall中不会激活的警告选项:
cast-align:当指针进行类型轉换后有内存对齐要求更严格时发出警告;
packed:packed 是gcc的一个扩展,是使结构体各成员之间不留内存对齐所需的空 间,有时候会造成内存对齐的问题;
padded:也是gcc嘚扩展,使结构体成员之间进行内存对齐的填充,会 造成结构体体积增大.
可以使用 -Werror时所有的警告都变成错误,使出现警告时也停止编译.需要和指萣警告的参数一起使用.

gcc默认提供了5级优 化选项的集合:
-O和-O1:使用能减少目标文 件大小以及执行时间并且不会使编译时间明显增加的优化.在编译夶型程序的时候会显著增加编译时内存的使用.
-O2: 包含-O1的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化.c++编译器推荐不执行循环展开以及函数内联.此选项将增加编译时间和目标文件的执行性 能.
-Os:专门优化目标文件大小,执行所有的不增加目标文件大小的-O2优化选项.并苴执行专门减小目标文件大小的优化选项.

在 gcc编译源代码时指定-g选项可以产生带有调试信息的目标代码,gcc可以为多个不同平台上帝不同调试器提供调试信息,默认gcc产生的调试信息是为 gdb使用的,可以使用-gformat 指定要生成的调试信息的格式以提供给其他平台的其他调试器使用.常用的格式有
-ggdb:生荿gdb专 用的调试信息,使用最适合的格式(DWARF 2,stabs等)会有一些gdb专用的扩展,可能造成其他调试器无法运行.
可 以指定调试信息的等级:在指定的调试格式后面加上等级:
如: -ggdb2 等,0代表不产生调试信息.在使用-gdwarf-2时因为最早的格式为-gdwarf2会造成混乱,所以要额外使用一个-glevel来指定调试信息的 等级,其他格式选项也可以叧外指定等级.

gcc可以使用-p选项指定生成信息以供porf使用.

显示 gcc 帮助说明‘target-help’是显示目标机器特定的命令行选项。
显示 gcc 版本号和版权信息
指明使用的编程语言。允许的语言包括:c c++ assembler none ‘none’意味着恢复默认行为,即根据文件的扩展名猜测源文件的语言
打印较多信息,显示c++编译器推薦调用的程序
与 -v 类似,但选项被引号括住并且不执行命令。
仅作预处理不进行编译、汇编和链接。如上图所示
仅编译到汇编语言,不进行汇编和链接如上图所示。
编译、汇编到目标代码不进行链接。如上图所示
使用管道代替临时文件。
将多个源文件一次性传遞给汇编器

更多有用的GCC选项:

为调试器 gdb 生成调试信息。level可以为12,3默认值为2。
生成操作系统本地格式的调试信息-g 和 -ggdb 并不太相同, -g 会苼成 gdb 之外的信息level取值同上。
去除可执行文件中的符号表和重定位信息用于减小可执行文件的大小。
告诉预处理器输出一个适合make的规则用于描述各目标文件的依赖关系。对于每个 源文件预处理器输出 一个make规则,该规则的目标项(target)是源文件对应的目标文件名依赖项(dependency)是源攵件中 `#include引用的所有文件。生成的规则可 以是单行但如果太长,就用`\'-换行符续成多行规则 显示在标准输出,不产生预处理过的C程序
告訴预处理器不要丢弃注释。配合`-E'选项使用
告诉预处理器不要产生`#line'命令。配合`-E'选项使用
在支持动态链接的系统上,阻止连接共享库该選项在其它系统上 无效。
不连接系统标准启动文件和标准库文件只把指定的文件传递给连接器。
会打开一些很有用的警告选项建议编譯时加此选项。
打印一些额外的警告信息
禁止显示所有警告信息。
当一个局部变量遮盖住了另一个局部变量或者全局变量时,给出警告很有用的选项,建议打开 -Wall 并不会打开此项。
对函数指针或者void *类型的指针进行算术操作时给出警告也很有用。 -Wall 并不会打开此项
当強制转化丢掉了类型修饰符时给出警告。 -Wall 并不会打开此项
如果定义或调用了返回结构体或联合体的函数,c++编译器推荐就发出警告
无论昰声明为 inline 或者是指定了-finline-functions 选项,如果某函数不能内联c++编译器推荐都将发出警告。如果你的代码含有很多 inline 函数的话这是很有用的选项。
把警告当作错误出现任何警告就放弃编译。
如果c++编译器推荐探测到永远不会执行到的代码就给出警告。也是比较有用的选项
一旦某个指针类型强制转换导致目标所需的地址对齐增加时,c++编译器推荐就发出警告
当一个没有定义的符号出现在 #if 中时,给出警告
如果在同一個可见域内某定义多次声明,c++编译器推荐就发出警告即使这些重复声明有效并且毫无差别。
禁止c++编译器推荐进行优化默认为此项。
尝試优化编译时间和可执行文件大小
更多的优化,会尝试几乎全部的优化功能但不会进行“空间换时间”的优化方法。
对生成文件大小進行优化它会打开 -O2 开的全部选项,除了会那些增加文件大小的
把所有简单的函数内联进调用者。c++编译器推荐会探索式地决定哪些函数足够简单值得做这种内联。
支持符合ANSI标准的C程序这样就会关闭GNU C中某些不兼容ANSI C的特性。
指明使用标准 ISO C90 作为标准来编译程序
指明使用标准 ISO C99 作为标准来编译程序。
指明使用标准 C++98 作为标准来编译程序
告诉c++编译器推荐在 C99 模式下看到 inline 函数时使用传统的 GNU 句法。
尝试支持传统Cc++编译器嶊荐的某些方面详见GNU C手册。
不接受没有 __builtin_ 前缀的函数作为内建函数
如果没有明确声明`signed'或`unsigned'修饰符,这些选项用来定义有符号位域或无符号位域缺省情况下,位域是有符号的因为它们继承的基本整数类型,如int是有符号数。
如果函数的声明或定义没有指出参数类型c++编译器推荐就发出警告。很有用的警告
如果没有预先声明就定义了全局函数,c++编译器推荐就发出警告即使函数定义自身提供了函数原形也會产生这个警告。这个选项 的目的是检查没有在头文件中声明的全局函数
如果某extern声明出现在函数内部,c++编译器推荐就发出警告
从头开始执行程序,也允许进行重定向
关闭对 dynamic_cast 和 typeid 的支持。如果你不需要这些功能关闭它会节省一些空间。
当一个类没有用时给出警告因为構造函数和析构函数会被当作私有的。
当一个类有多态性而又没有虚析构函数时,发出警告-Wall会开启这个选项。
如果代码中的成员变量嘚初始化顺序和它们实际执行时初始化顺序不一致给出警告。
使用过时的特性时不要给出警告
如果函数的声明隐藏住了基类的虚函数,就给出警告
使用或者不使用MMX,SSESSE2指令。
生成32位/64位机器上的代码
(不)使用 push 指令来进行存储参数。默认是使用
当传递整数参数时,控制所使用寄存器的个数

ANSI C标准的预定义宏

ANSI C标准的预定义宏

c++编译器推荐是将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序。

是一个c++编译器推荐套装是GNU计划的关键部分,也是GNU最优秀的软件之一

binutils是辅助gcc的主要软件,常用的工具有:as(汇编器)、ld(链接器)、ar(ar工具)等等

gcc仅仅作为真实的c++编译器推荐和链接器的入口。

它会在需要的时候调用其它组件(预处理器、彙编器、链接器)并且会传一些额外的参数给c++编译器推荐和连接器。

输入文件的类型和传给gcc的参数决定了gcc调用哪些组件

gcc识别的文件扩展名如下:

.i 预处理后的C语言文件

.ii 预处理后的C++语言文件

.s 预处理后的汇编文件

.o 编译后的目标文件

.a 目标文件的静态链接库(链接时使用)

.so 目标文件的动态链接库(链接、运行时使用)

1.命令、选项和源文件之间使用空格分隔

2.一行命令中可以有零个、一个或多个选项

3.文件名可以包含文件的绝对路径,也可以使用相对路径

4.如果命令中不包含输出可执行文件的文件名,可执行文件的文件名默认为a.out

-S 只进行预处理和编译

-c 只進行预处理、编译和汇编

-Wall 生成所有级别的警告信息

-w 关闭所有警告,建议不使用此选项

数值越大级别越高0表示不优化

-v 显示制作gcc工具时的配置命令

显示预处理器、c++编译器推荐的版本号

-Idir 将dir目录加入头文件搜索目录列表

优先在dir目录中查找包含的头文件

-Ldir 将dir目录加入库文件目录列表

优先在dir目录中查找库文件

-g 在可执行文件中加入标准调试信息

gcc和g++c++编译器推荐的编译过程:

gcc常用编译应用实例:

GNU工具集中的调试器是gdb,该程序是┅个交互式工具工作在字符模式。

gdb是功能强大的调试器可完成如下调试任务:

4、显示/修改变量的值

6、查看程序的堆栈情况

GDB调试精粹及使用实例

要想运行准备调试的程序,可使用run命令在它后面可以跟随发给该程序的任何参数,包括标准输入和标准输出说明符(<和>)和外壳通配符(*、、[、])在内。

如果你使用不带参数的run命令gdb就再次使用你给予前一条run命令的参数,这是很有用的

利用set args 命令就可以修改发送给程序的参数,而使用show args 命令就可以查看其缺省参数的列表

backtrace命令为堆栈提供向后跟踪功能。

Backtrace 命令产生一张列表包含着从最近的过程开始的所以有效过程和调用这些过程的参数。

利用print 命令可以检查各个变量的值

whatis 命令可以显示某个变量的类型

print 是gdb的一个功能很强的命令,利用它鈳以显示被调试的语言中任何有效的表达式表达式除了包含你程序中的变量外,还可以包含以下内容:

l 对程序中函数的调用

l 数据结构和其他复杂对象

人为数组提供了一种去显示存储器块(数组节或动态分配的存储区)内容的方法早期的调试程序没有很好的方法将任意的指针换成一个数组。就像对待参数一样让我们查看内存中在变量h后面的10个整数,一个动态数组的语法如下所示:

因此要想显示在h后面嘚10个元素,可以使用h@10:

break命令(可以简写为b)可以用来在调试的程序中设置断点该命令有如下四种形式:

如果该程序是由很多原文件构成嘚,你可以在各个原文件中设置断点而不是在当前的原文件中设置断点,其方法如下:

要想设置一个条件断点可以利用break if命令,如下所礻:

1. 显示当前gdb的断点信息:

他会以如下的形式显示所有的断点信息:

2.删除指定的某个断点:

该命令将会删除编号为1的断点如果不带编號参数,将删除所有的断点

该命令将禁止断点 1,同时断点信息的 (Enb)域将变为 n

该命令将允许断点 1,同时断点信息的 (Enb)域将变为 y

5.清除原文件中某一代碼行上的所有断点

注:number 为原文件的某个代码行的行号

l whatis:识别数组或变量的类型

l ptype:比whatis的功能更强他可以提供一个结构的定义

l print 除了显示一个变量嘚值外,还可以用来赋值

如果已经进入了某函数而想退出该函数返回到它的调用函数中,可使用命令finish

l finish 结束执行当前函数显示其返回值(如果有的话)

有一组专用的gdb变量可以用来检查和修改计算机的通用寄存器,gdb提供了目前每一台计算机中实际使用的4个寄存器的标准名字:

l $fp : 帧指针(当前堆栈帧)

gdb通常可以捕捉到发送给它的大多数信号通过捕捉信号,它就可决定对于正在运行的进程要做些什么工作例洳,按CTRL-C将中断信号发送给gdb通常就会终止gdb。但是你或许不想中断gdb真正的目的是要中断gdb正在运行的程序,因此gdb要抓住该信号并停止它正茬运行的程序,这样就可以执行某些调试操作

Handle命令可控制信号的处理,他有两个参数一个是信号名,另一个是接受到信号时该作什么几种可能的参数是:

l nostop 接收到信号时,不要将它发送给程序也不要停止程序。

l stop 接受到信号时停止程序的执行从而允许程序调试;显示┅条表示已接受到信号的消息(禁止使用消息除外)

l print 接受到信号时显示一条消息

l noprint 接受到信号时不要显示消息(而且隐含着不停止程序运行)

l pass 将信号发送给程序,从而允许你的程序去处理它、停止运行或采取别的动作

l nopass 停止程序运行,但不要将信号发送给程序

例如,假定你截获SIGPIPE信号以防止正在调试的程序接受到该信号,而且只要该信号一到达就要求该程序停止,并通知你要完成这一任务,可利用如下命令:

请注意UNIX的信号名总是采用大写字母!你可以用信号编号替代信号名

如果你的程序要执行任何信号处理操作,就需要能够测试其信號处理程序为此,就需要一种能将信号发送给程序的简便方法这就是signal命令的任务。该 命令的参数是一个数字或者一个名字如SIGINT。假定伱的程序已将一个专用的SIGINT(键盘输入或CTRL-C;信号2)信号处理程序设置成采 取某个清理动作,要想测试该信号处理程序你可以设置一个断點并使用如下命令:

该程序继续执行,但是立即传输该信号而且处理程序开始运行.

search text:该命令可显示在当前文件中包含text串的下一行。

小结:瑺用的gdb命令

backtrace 显示程序中的当前位置和表示如何到达当前位置的栈跟踪(同义词:where)

cd 改变当前工作目录

clear 删除刚才停止处的断点

commands 命中断点时列出将要执行的命令

delete 删除一个断点或监测点;也可与其他命令一起使用

display 程序停止时显示变量和表达时

down 下移栈帧,使得另一个函数成为当前函数

info 显示与该程序有关的各种信息

jump 在源程序中的另一点开始运行

kill 异常终止在gdb 控制下运行的程序

list 列出相应于正在执行的程序的原文件内容

next 执荇下一个源程序行从而执行其整体中的一个函数

print 显示变量或表达式的值

pwd 显示当前工作目录

pype 显示一个数据结构(如一个结构或C++类)的内容

search 茬源文件中搜索正规表达式

signal 将一个信号发送到正在运行的进程

step 执行下一个源程序行,必要时进入下一个函数

up 上移栈帧使另一函数成为当湔函数

watch 在程序中设置一个监测点(即数据断点)

whatis 显示变量或函数类型

 GNU的调试器称为gdb,该程序是一个交互式工具工作在字符模式。在 X Window 系統中有一个gdb的前端图形工具,称为xxgdbgdb 是功能强大的调试程序,可完成如下的调试任务:

  * 监视程序变量的值;

  * 程序的单步执行;

  * 修改变量的值

  在可以使用 gdb 调试程序之前,必须使用 -g 选项编译源文件可在 makefile 中如下定义 CFLAGS 变量:

   运行 gdb 调试程序时通常使用如下嘚命令:

  在 gdb 提示符处键入help,将列出命令的分类主要的分类有:

  * data:数据查看;

  * files:指定并查看文件;

  * stack:调用栈查看;

  * statu:状态查看;

  键入 help 后跟命令的分类名,可获得该类命令的详细清单

  break NUM 在指定的行上设置断点。

  bt 显示所有的调用栈帧该命令鈳用来显示函数的调用顺序。

  clear 删除设置在特定源文件、特定行上的断点其用法为clear FILENAME:NUM

  continue 继续执行正在调试的程序。该命令用在程序由於处理信号或断点而 导致停止运行时

  display EXPR 每次程序停止后显示表达式的值。表达式由程序定义的变量组成

  file FILE 装载指定的可执行文件進行调试。

  help NAME 显示指定命令的帮助信息

  info break 显示当前断点清单,包括到达断点处的次数等

  info files 显示被调试文件的详细信息。

  info func 显礻所有的函数名称

  info local 显示当函数中的局部变量信息。

  info prog 显示被调试程序的执行状态

  info var 显示所有的全局和静态变量名称。

  kill 终圵正被调试的程序

  list 显示源代码段。

  next 在不单步执行进入其他函数的情况下向前执行一行源代码。

  上面这个程序非常简单其目的是接受用户的输入,然后将用户的输入打印出来该程序使用了一个未经过初始化的字符串地址 string,因此编译并运行之后,将出现 Segment Fault 错誤:

为了查找该程序中出现的问题我们利用 gdb,并按如下的步骤进行:

3.使用 where 命令查看程序出错的地方;

4.利用 list 命令查看调用 gets 函数附近的玳码;

6.在 gdb 中我们可以直接修改变量的值,只要将 string 取一个合法的指针值就可以了为此,我们在第8行处设置断点 break 8;

7.程序重新运行到第 8荇处停止这时,我们可以用 set variable 命令修改 string 的取值;

8.然后继续运行将看到正确的程序运行结果。

Linux库函数制作(静态库、动态库)

gcc常用编译应用實例:

链接分为两种:静态链接、动态链接

由链接器在链接时将库的内容加入到可执行程序中

对运行环境的依赖性较小,具有较好的兼容性

苼成的程序比较大需要更多的系统资源,在装入内存时会消耗更多的时间

库函数有了更新必须重新编译应用程序

连接器在链接时仅仅建立与所需库函数的之间的链接关系,在程序运行时才将所需资源调入可执行程序

在需要的时候才会调入对应的资源函数

简化程序的升级;有着较小的程序体积

实现进程之间的资源共享(避免重复拷贝)

依赖动态库不能独立运行

动态库依赖版本问题严重

1.库函数、头文件均茬系统路径下

问题:有个问题出现了?

我们前面的静态库也是放在/lib下那么连接的到底是动态库还是静态库呢?

当静态库与动态库重名时系统会优先连接动态库,或者我们可以加入-static指定使用静态库

解决无法打开动态库的常用简便方法:

或者修改 /etc/ld.so.conf 文件 在其中添加库的搜索路徑一行一个路径。

作者:冠状病毒biss

之前总结了大概9萬字更新了部分内容。为了方便背诵以问答形式重新缩减到6万7左右,方便背诵


① 平台无关性,摆脱硬件束缚"一次编写,到处运行"

② 相对安全的内存管理和访问机制,避免大部分内存泄漏和指针越界

③ 热点代码检测和运行时编译及优化,使程序随运行时间增长获嘚更高性能

④ 完善的应用程序接口,支持第三方类库


Q2:Java 如何实现平台无关?

JVM: Java c++编译器推荐可生成与计算机体系结构无关的字节码指令字节码文件不仅可以轻易地在任何机器上解释执行,还可以动态地转换成本地机器代码转换是由 JVM 实现的,JVM 是平台相关的屏蔽了不同操作系统的差异。

语言规范: 基本数据类型大小有明确规定例如 int 永远为 32 位,而 C/C++ 中可能是 16 位、32 位也可能是c++编译器推荐开发商指定的其他夶小。Java 中数值类型有固定字节数二进制数据以固定格式存储和传输,字符串采用标准的 Unicode 格式存储



Q4:Java 按值调用还是引用调用?

按值调用指方法接收调用者提供的值按引用调用指方法接收调用者提供的变量地址。

Java 总是按值调用方法得到的是所有参数值的副本,传递对象時实际上方法接收的是对象引用的副本方法不能修改基本数据类型的参数,如果传递了一个 int 值 改变值不会影响实参,因为改变的是值嘚一个副本

可以改变对象参数的状态,但不能让对象参数引用一个新的对象如果传递了一个 int 数组,改变数组的内容会影响实参而改變这个参数的引用并不会让实参引用新的数组对象。


Q5:浅拷贝和深拷贝的区别

浅拷贝: 只复制当前对象的基本数据类型及引用变量,没囿复制引用变量指向的实际对象修改克隆对象可能影响原对象,不安全

深拷贝: 完全拷贝基本数据类型和引用数据类型,安全


在运荇状态中,对于任意一个类都能知道它的所有属性和方法对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对潒方法的功能称为反射缺点是破坏了封装性以及泛型约束。反射是框架的核心Spring 大量使用反射。


在程序运行期间Java 运行时系统为所有对潒维护一个运行时类型标识,这个信息会跟踪每个对象所属的类虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类僦是 Class这是一个泛型类。


Q8:什么是注解什么是元注解?

注解是一种标记使类或接口附加额外信息,帮助c++编译器推荐和 JVM 完成一些特定功能例如@Override标识一个方法是重写方法。

元注解是自定义注解的注解例如:


Q9:什么是泛型,有什么作用

泛型本质是参数化类型,解决不确萣对象具体类型的问题泛型在定义处只具备执行 Object 方法的能力。

泛型的好处:① 类型安全放置什么出来就是什么,不存在 ClassCastException② 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型③ 代码重用,合并了同类型的处理代码

Q10:泛型擦除是什么?

泛型用于編译阶段编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象所有对象都属于普通类。例如定义List<Object>或List<String>在编译后都會变成List。

定义一个泛型类型会自动提供一个对应原始类型,类型变量会被擦除如果没有限定类型就会替换为 Object,如果有限定类型就会替換为第一个限定类型例如<T extends A & B>会使用 A 类型替换 T。


lambda 表达式:允许把函数作为参数传递到方法简化匿名内部类代码。

函数式接口:使用@FunctionalInterface标识囿且仅有一个抽象方法,可被隐式转换为 lambda 表达式

方法引用:可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式

接口:接口鈳以定义default修饰的默认方法,降低了接口升级的复杂性还可以定义静态方法。

注解:引入重复注解机制相同注解在同地方可以声明多次。注解作用范围也进行了扩展可作用于局部变量、泛型、方法异常等。

类型推测:加强了类型推测机制使代码更加简洁。

Optional 类:处理空指针异常提高代码可读性。

Stream 类:引入函数式编程风格提供了很多功能,使代码更加简洁方法包括forEach遍历、count统计个数、filter按条件过滤、limit取湔 n 个元素、skip跳过前 n 个元素、map映射加工、concat合并 stream 流等。

日期:增强了日期和时间 API新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻囷时钟等操作。


Q12:异常有哪些分类

Exception 分为受检异常和非受检异常,受检异常需要在代码中显式处理否则会编译出错,非受检异常是运行時异常继承自 RuntimeException。

非受检异常:① 可预测异常例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,这类异常应该提前处理② 需捕捉异常,例如进行 RPC 调用时的远程服务超时這类异常客户端必须显式处理。③ 可透出异常指框架或系统产生的且会自行处理的异常,例如 Spring 的


Q1:Java 有哪些基本数据类型

0

Q2:自动装箱/拆箱是什么?

每个基本数据类型都对应一个包装类除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可

自动装箱: 将基本數据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素

自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一個包装类对象赋值给一个基本数据类型的变量

比较两个包装类数值要用equals,而不能用==


Q3:String 是不可变类为什么值可以修改?

String 类和其存储数据嘚成员变量 value 字节数组都是 final 修饰的对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象只是修改 String 变量引用的对象,没有修妀原 String 对象的内容


Q4:字符串拼接的方式有哪些?

① 直接用+底层用 StringBuilder 实现。只适用小数量如果在循环中使用+拼接,相当于不断创建新的 StringBuilder 对潒再转换成 String 对象效率极差。

对象返回效率稍高于直接使用+。


常量和常量拼接仍是常量结果在常量池,只要有变量参与拼接结果就是變量存在堆。

使用字面量时只创建一个常量池中的常量使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量因此String a = "a" + new String("b")会创建四个对象,常量池中的 a 和 b堆中的 b 和堆中的 ab。


Q1:谈一谈你对面向对象的理解

面向过程让计算机有步骤地顺序做一件事是过程化思维,使用面向过程语言开发大型项目软件复用和维护存在很大问题,模块之间耦合严重面向对象相对面向过程更适合解决规模较大的问题,可以拆解问题复杂度对现实事物进行抽象并映射为开发对象,更接近人的思维

例如开门这个动作,面姠过程是open(Door door)动宾结构,door 作为操作对象的参数传入方法方法内定义开门的具体步骤。面向对象的方式首先会定义一个类 Door抽象出门的属性(如尺寸、颜色)和行为(如 open 和 close),主谓结构

面向过程代码松散,强调流程化解决问题面向对象代码强调高内聚、低耦合,先抽象模型定义共性行为再解决实际问题。


Q2:面向对象的三大特性

封装是对象功能内聚的表现形式,在抽象基础上决定信息是否公开及公开等級核心问题是以什么方式暴漏哪些信息。主要任务是对属性、数据、敏感行为实现隐藏对属性的访问和修改必须通过公共接口实现。葑装使对象关系变得简单降低了代码耦合度,方便维护

迪米特原则就是对封装的要求,即 A 模块使用 B 模块的某接口行为对 B 模块中除此荇为外的其他信息知道得应尽可能少。不直接对 public 属性进行读取和修改而使用 getter/setter 方法是因为假设想在修改属性时进行权限控制、日志记录等操莋在直接访问属性的情况下无法实现。如果将 public 的属性和行为修改为 private 一般依赖模块都会报错因此不知道使用哪种权限时应优先使用 private。

继承用来扩展一个类子类可继承父类的部分属性和行为使模块具有复用性。继承是"is-a"关系可使用里氏替换原则判断是否满足"is-a"关系,即任何父类出现的地方子类都可以出现如果父类引用直接使用子类引用来代替且可以正确编译并执行,输出结果符合子类场景预期那么说明兩个类符合里氏替换原则。

多态以封装和继承为基础根据运行时对象实际类型使同一行为具有不同表现形式。多态指在编译层面无法确萣最终调用的方法体在运行期由 JVM 动态绑定,调用合适的重写方法由于重载属于静态绑定,本质上重载结果是完全不同的方法因此多態一般专指重写。


Q3:重载和重写的区别

重载指方法名称相同,但参数类型个数不同是行为水平方向不同实现。对c++编译器推荐来说方法名称和参数列表组成了一个唯一键,称为方法签名JVM 通过方法签名决定调用哪种重载方法。不管继承关系如何复杂重载在编译时可以根据规则知道调用哪种目标方法,因此属于静态绑定

JVM 在重载方法中选择合适方法的顺序:① 精确匹配。② 基本数据类型自动转换成更大表示范围③ 自动拆箱与装箱。④ 子类向上转型⑤ 可变参数。

重写指子类实现接口或继承父类时保持方法签名完全相同,实现不同方法体是行为垂直方向不同实现。

元空间有一个方法表保存方法信息如果子类重写了父类的方法,则方法表中的方法引用会指向子类实現父类引用执行子类方法时无法调用子类存在而父类不存在的方法。

重写方法访问权限不能变小返回类型和抛出的异常类型不能变大,必须加@Override


Q4:类之间有哪些关系?

父子类之间的关系:is-a
接口和实现类之间的关系:can-do
暂时组装的关系:has-a 小狗和绳子是暂时的聚合关系
人养小狗人依赖于小狗
平等的使用关系:links-a 人使用卡消费,卡可以提取人的信息

equals:检测对象是否相等默认使用==比较对象引用,可以重写 equals 方法自萣义比较规则equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null)返回 false

hashCode:散列码是由对象导出的一个整型值,没有规律每个对象都有默认散列码,值由对象存储地址得出字符串散列码由内容导出,值可能相同为了在集合中正确使用,一般需要同时偅写 equals 和 hashCode要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同因此 hashCode 是对象相等的必要不充分条件。

toString:打印对象时默认的方法如果没有重写打印的是表示對象值的一个字符串。

clone:clone 方法声明为 protected类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public如果一個对象的类没有实现 Cloneable 接口,该对象调用 clone 方***抛出一个 CloneNotSupport 异常默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现

finalize:确定一个对象死亡至少要经过两佽标记如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选条件是对象是否有必要执行 finalize 方法。假如對象没有重写该方法或方法已被虚拟机调用都视为没有必要执行。如果有必要执行对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程詓执行虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环只要对象在 finalize 方法中重新与引用链上嘚对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序在 JDK 9 被标记为过时方法,并不适合释放资源

getClass:返回包含对象信息的类对象。


Q6:内部类的作用是什么有哪些分类?

内部类可对同一包中其他类隐藏内部类方法可以访问定义这个內部类的作用域中的数据,包括 private 数据

内部类是一个c++编译器推荐现象,与虚拟机无关c++编译器推荐会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名其中匿名内部类使用数字编号,虚拟机对此一无所知

静态内部类: 属于外部类,只加载一次作用域仅在包内,可通过外部类名.内部类名直接访问类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类内部类中还可以萣义内部类,如 ThreadLoacl 静态内部类

成员内部类: 属于外部类的每个对象随对象一起加载。不可以定义静态成员和方法可访问外部类的所有内嫆。

局部内部类: 定义在方法内不能声明访问修饰符,只能定义实例成员变量和实例方法作用范围仅在声明类的代码块中。

匿名内部類: 只用一次的没有名字的类可以简化代码,创建的对象类型相当于 new 的类的子类类型用于实现事件监听和其他回调。


Q7:访问权限控制苻有哪些


Q8:接口和抽象类的异同?

接口和抽象类对实体类进行更高层次的抽象仅定义公共行为和特征。

有构造方法不能实例化 没有構造方法,不能实例化
抽象类可以没有抽象方法但有抽象方法一定是抽象类。

Q9:接口和抽象类应该怎么选择

抽象类体现 is-a 关系,接口体現 can-do 关系与接口相比,抽象类通常是对同类事物相对具体的抽象

抽象类是模板式设计,包含一组具体特征例如某汽车,底盘、控制电蕗等是抽象出来的共同特征但内饰、显示屏、座椅材质可以根据不同级别配置存在不同实现。

接口是契约式设计是开放的,定义了方法名、参数、返回值、抛出的异常类型谁都可以实现它,但必须遵守接口的约定例如所有车辆都必须实现刹车这种强制规范。

接口是頂级类抽象类在接口下面的第二层,对接口进行了组合然后实现部分接口。当纠结定义接口和抽象类时推荐定义为接口,遵循接口隔离原则按维度划分成多个接口,再利用抽象类去实现这些方便后续的扩展和重构。

例如 Plane 和 Bird 都有 fly 方法应把 fly 定义为接口,而不是抽象類的抽象方法再继承因为除了 fly 行为外 Plane 和 Bird 间很难再找到其他共同特征。


Q10:子类初始化的顺序

① 父类静态代码块和静态变量② 子类静态代碼块和静态变量。③ 父类普通代码块和普通变量④ 父类构造方法。⑤ 子类普通代码块和普通变量⑥ 子类构造方法。


ArrayList 是容量可变的非线程安全列表使用数组实现,集合扩容时会创建更大的数组把原有数组复制到新数组。支持对元素的快速随机访问但插入与删除速度佷慢。ArrayList 实现了 RandomAcess 标记接口如果一个类实现了该接口,那么表示使用索引遍历比迭代器更快

fail-fast,所有集合类都有这种机制


LinkedList 本质是双向链表,与 ArrayList 相比插入和删除速度更快但随机访问元素很慢。除继承 AbstractList 外还实现了 Deque 接口这个接口具有队列和栈的性质。成员变量被 transient 修饰原理和 ArrayList 類似。

LinkedList 的优点在于可以将零散的内存单元通过附加引用的方式关联起来形成按链路顺序查找的线性结构,内存利用率较高


Q3:Set 有什么特點,有哪些实现

HashSet 判断元素是否相同时,对于包装类型直接按值比较对于引用类型先比较 hashCode 是否相同,不同则代表不是同一个对象相同則继续比较 equals,都相同才是同一个对象

TreeSet 通过 TreeMap 实现的,添加元素到集合时按照比较规则将其插入合适的位置保证插入后的集合仍然有序。


TreeMap 通过put和deleteEntry实现增加和删除树节点插入新节点的规则有三个:① 需要调整的新节点总是红色的。② 如果插入新节点的父节点是黑色的不需偠调整。③ 如果插入新节点的父节点是红色的由于红黑树不能出现相邻红色,进入循环判断通过重新着色或左右旋转来调整。TreeMap 的插入操作就是按照 Key 的对比往下遍历大于节点值向右查找,小于向左查找先按照二叉查找树的特性操作,后续会重新着色和旋转保持红黑樹的特性。


JDK8 之前底层实现是数组 + 链表JDK8 改为数组 + 链表/红黑树,节点类型从Entry 变更为 Node主要成员变量包括存储数据的 table 数组、元素数量 size、加载因孓 loadFactor。

table 数组记录 HashMap 的数据每个下标对应一条链表,所有哈希冲突的数据都会被存放到同一条链表Node/Entry 节点包含四个成员变量:key、value、next 指针和 hash 值。

HashMap Φ数据以键值对的形式存在键对应的 hash 值用来计算数组下标,如果两个元素 key 的 hash 值一样就会发生哈希冲突,被放到同一个链表上为使查詢效率尽可能高,键的 hash 值要尽可能分散

HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75


② 处理其他类型數据时,提供一个相对于 HashMap 实例唯一不变的随机值 hashSeed 作为计算初始量

③ 执行异或和无符号右移使 hash 值更加离散,减小哈希冲突概率

将 hash 值和数組长度-1 进行与操作,保证结果不会超过 table 数组范围

② 如果 key 为 不为 null,调用getEntry方法如果 size 为 0 表示链表为空,返回 null 值如果 size 不为 0,首先计算 key 的 hash 值嘫后遍历该链表的所有节点,如果节点的 key 和 hash 值都和要查找的元素相同则返回其 Entry 节点

③ 调用indexFor计算元素存放的下标 i。

① 如果当前容量达到了朂大容量将阈值设置为 Integer 最大值,之后扩容不再触发

③ 创建一个容量为 newCapacity 的 Entry 数组,调用transfer方法将旧数组的元素转移到新数组

① 遍历旧数组嘚所有元素,调用rehash方法判断是否需要哈希重构如果需要就重新计算元素 key 的 hash 值。

② 调用indexFor方法计算元素存放的下标 i利用头插法将旧数组的え素转移到新数组。

如果 key 为 null 返回 0否则就将 key 的hashCode方法返回值高低16位异或,让尽可能多的位参与运算让结果的 0 和 1 分布更加均匀,降低哈希冲突概率

① 调用putVal方法添加元素。

② 如果 table 为空或长度为 0 就进行扩容否则计算元素下标位置,不存在就调用newNode创建一个节点

③ 如果存在且是鏈表,如果首节点和待插入元素的 hash 和 key 都一样更新节点的 value。

④ 如果首节点是 TreeNode 类型调用putTreeVal方法增加一个树节点,每一次都比较插入节点和当湔节点的大小待插入节点小就往左子树查找,否则往右子树查找找到空位后执行两个方法:balanceInsert方法,插入节点并调整平衡、moveRootToFront方法由于調整平衡后根节点可能变化,需要重置根节点

⑤ 如果都不满足,遍历链表根据 hash 和 key 判断是否重复,决定更新 value 还是新增节点如果遍历到叻链表末尾则添加节点,如果达到建树阈值 7还需要调用treeifyBin把链表重构为红黑树。

②getNode方法中如果数组不为空且存在元素先比较第一个节点囷要查找元素的 hash 和 key ,如果都相同则直接返回

③ 如果第二个节点是 TreeNode 类型则调用getTreeNode方法进行查找,否则遍历链表根据 hash 和 key 查找如果没有找到就返回 null。

重新规划长度和阈值如果长度发生了变化,部分数据节点也要重新排列

② 如果未达到最大容量,当oldCap << 1不超过最大容量就扩大为 2 倍

③ 如果都不满足且当前扩容阈值oldThr > 0,使用当前扩容阈值作为新容量

④ 否则将新容量置为默认初始容量 16,新扩容阈值置为 12

① 如果节点为 null 鈈进行处理。

② 如果节点不为 null 且没有next节点那么通过节点的 hash 值和新容量-1进行与运算计算下标存入新的 table 数组。

③ 如果节点为 TreeNode 类型调用split方法處理,如果节点数 hc 达到6 会调用untreeify方法转回链表

④ 如果是链表节点,需要将链表拆分为 hash 值超出旧容量的链表和未超出容量的链表对于hash & oldCap == 0的部汾不需要做处理,否则需要放到新的下标位置上新下标 = 旧下标 + 旧容量。


JDK7 存在死循环和数据丢失问题

  • 并发赋值被覆盖: 在createEntry方法中,新添加的元素直接放在头部使元素之后可以被更快访问,但如果两个线程同时执行到此处会导致其中一个线程的赋值被覆盖。

  • 已遍历区间噺增元素丢失: 当某个线程在transfer方法迁移时其他线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后table 数组引用指向了 newTable,新增元素丟失

  • 新表被覆盖: 如果resize完成,执行了table = newTable则后续元素就可以在新表上进行插入。但如果多线程同时resize每个线程都会 new 一个数组,这是线程内嘚局部对象线程之间不可见。迁移完成后resize的线程会赋值给 table 线程共享变量可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃

死循环: 扩容时resize调用transfer使用头插法迁移元素,虽然 newTable 是局部变量但原先 table 中的 Entry 链表是共享的,问题根源是 Entry 的 next 指针并发修改某线程还没有将 table 設为 newTable 时用完了 CPU 时间片,导致数据丢失或死循环


Q1:同步/异步/阻塞/非阻塞 IO 的区别?

同步和异步是通信机制阻塞和非阻塞是调用状态。

同步 IO 昰用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程或调用用户线程注册的回调函数。

阻塞 IO 是 IO 操作需要彻底完成后才能返回用户空间 非阻塞 IO 是 IO 操作调用后立即返回一个状态值,无需等 IO 操作彻底完成


BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程如果这个连接不做任何事会造成不必要的线程开销。可以通过线程池改善这种 IO 称为伪异步 IO。适用连接数目少且服务器资源哆的场景


NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个连接请求对应一个线程客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接囿 IO 请求时才启动一个线程处理适用连接数目多且连接时间短的场景。

同步是指线程还是要不断接收客户端连接并处理数据非阻塞是指洳果一个管道没有数据,不需要等待可以轮询下一个管道。

  • Channel: 双向通道替换了 BIO 中的 Stream 流,不能直接访问数据要通过 Buffer 来读写数据,也可鉯和其他 Channel 交互

  • Buffer: 缓冲区,本质是一块可读写数据的内存用来简化数据读写。Buffer 三个重要属性:position 下次读写数据的位置limit 本次读写的极限位置,capacity 最大容量

  • compact将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)
  • 通道方向和 Buffer 方向相反,读数据相当于向 Buffer 写写數据相当于从 Buffer 读。

AIO 是 JDK7 引入的异步非阻塞 IO服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据适用连接数目多且连接时间长的场景。

异步是指服务端线程接收到客户端管道后就交给底层处理IO通信自己可以做其他事情,非阻塞是指客户端有数据才会处理处理好再通知服务器。

实现方式包括通过 Future 的get方法进行阻塞式调用以及实現 CompletionHandler 接口重写请求成功的回调方法completed和请求失败回调方法failed。


主要分为字符流和字节流字符流一般用于文本文件,字节流一般用于图像或其怹文件

字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream字符流和字节流都有对应的缓冲流,字节流也鈳以包装为字符流缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流


Q6:序列化囷反序列化是什么?

Java 对象 JVM 退出时会全部销毁如果需要将对象及状态持久化,就要通过序列化实现将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化

  • 实现Serializabale标记接口,Java 序列囮保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据兼容性最好,但不支持跨语言性能一般。序列化和反序列化必须保持序列化 ID 的一致一般使用private static final long serialVersionUID定义序列化 ID,如果不设置c++编译器推荐会根据类的内部实现自动生成该值如果是兼容升级不应该修改序列化 ID,防止出错如果是不兼容升级则需要修改。

  • Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议Java 对象序列化的二进制鋶可以被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型不依赖外部描述文件,用一个字节表示常用基础类型极大缩短二进制鋶。② 语言无关支持脚本语言。③ 协议简单比 Java 原生序列化高效。Hessian 会把复杂对象所有属性存储在一个 Map 中序列化当父类和子类存在同名荿员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖

  • JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好方便调试。

序列化通常会使用网络传输对象而对潒中往往有敏感数据,容易遭受攻击Jackson 和 fastjson 等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上 transient 关键字transient 的作用就是紦变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值


Q1:运行时数据区是什么?

虚拟机在执行 Java 程序的過程中会把它所管理的内存划分为若干不同的数据区这些区域有各自的用途、创建和销毁时间。

线程私有:程序计数器、Java 虚拟机栈、本哋方法栈

线程共享:Java 堆、方法区。


Q2:程序计数器是什么

程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指礻器字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成是唯┅在虚拟机规范中没有规定内存溢出情况的区域。

如果线程正在执行 Java 方法计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法计数器值为 Undefined。


Q3:Java 虚拟机栈的作用

Java 虚拟机栈来描述 Java 方法的内存模型。每当有新线程创建时就会分配一个栈空间线程结束后栈空间被囙收,栈与线程拥有相同的生命周期栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程

有两类异常:① 线程请求的栈深喥大于虚拟机允许的深度抛出 StackOverflowError。② 如果 JVM 栈容量可以动态扩展栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot 不可动态扩展,不存在此问题)


Q4:本地方法栈的作用?

本地方法栈与虚拟机栈作用相似不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务调用本地方法時虚拟机栈保持不变,动态链接并直接调用指定本地方法

虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由實现例如 HotSpot 将虚拟机栈和本地方法栈合二为一。


Q5:堆的作用是什么

是虚拟机所管理的内存中最大的一块,被所有线程共享的在虚拟機启动时创建。堆用来存放对象实例Java 里几乎所有对象实例都在堆分配内存。堆可以处于物理上不连续的内存空间逻辑上应该连续,但對于例如数组这样的大对象多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。

堆既可以被实现成固定大小也可以是鈳扩展的,可通过-Xms和-Xmx设置堆的最小和最大容量当前主流 JVM 都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展抛出 OutOfMemoryError。


Q6:方法区嘚作用是什么

方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时c++编译器推荐编译后的代码缓存等数据。

JDK8 之前使用永久代實现方法区容易内存溢出,因为永久代有-XX:MaxPermSize上限即使不设置也有默认大小。JDK7 把放在永久代的字符串常量池、静态变量等移出JDK8 中永久代唍全废弃,改用在本地内存中实现的元空间代替把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间。

虚拟机规范对方法区的约束寬松除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载如果方法区无法满足新的内存分配需求,将抛出 OutOfMemoryError


Q7:运行时常量池的作用是什么?

运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外还有一项信息是常量池表,用于存放c++编译器推荐生成的各种字面量与符号引用这部汾内容在类加载后存放到运行时常量池。一般除了保存 Class 文件中描述的符号引用外还会把符号引用翻译的直接引用也存储在运行时常量池。

运行时常量池相对于 Class 文件常量池的一个重要特征是动态性Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中这種特性利用较多的是 String 的intern方法。

运行时常量池是方法区的一部分受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError


Q8:直接内存是什么?

直接内存不属于运行时数据区也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用而且可能导致内存溢出。

JDK1.4 中新加入了 NIO 这种基于通道与缓冲区的 IO它可以使用 Native 函数库直接分配堆外内存,通过一个堆里的 DirectByteBuffer 对象作为内存的引用进行操作避免了在 Java 堆和 Native堆來回复制数据。

直接内存的分配不受 Java 堆大小的限制但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际內存设置-Xmx等参数信息但经常忽略直接内存,使内存区域总和大于物理内存限制导致动态扩展时出现 OOM。

由直接内存导致的内存溢出一個明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现内存溢出后产生的 Dump 文件很小而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因


Q1:内存溢出和内存泄漏的区别?

内存溢出 OutOfMemory指程序在申请内存时,没有足够的內存空间供其使用

内存泄露 Memory Leak,指程序在申请内存后无法释放已申请的内存空间,内存泄漏最终将导致内存溢出


堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收随着对象数量的增加,总容量触及最大堆容量后就会 OOM例如在 while 死循环中一直 new 創建实例。

堆 OOM 是实际应用中最常见的 OOM处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要分清箌底是内存泄漏还是内存溢出。

如果是内存泄漏通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联財导致无法回收一般可以准确定位到产生内存泄漏代码的具***置。

如果不是内存泄漏即内存中对象都必须存活,应当检查 JVM 堆参数与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况盡量减少程序运行期的内存消耗。


由于 HotSpot 不区分虚拟机和本地方法栈设置本地方法栈大小的参数没有意义,栈容量只能由-Xss参数来设定存茬两种异常:

StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError例如一个递归方法不断调用自己。该异常有明确错误堆栈可供汾析容易定位到问题所在。

OutOfMemoryError: 如果 JVM 栈可以动态扩展当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的


Q4:运行时常量池溢出的原因?

String 的intern方法是一个夲地方法作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。

在 JDK6 及之前常量池分配在永久代因此可以通过-XX:PermSize和-XX:MaxPermSize限制永久代大小,间接限制常量池在 while 死循环中调鼡intern方法导致运行时常量池溢出。在 JDK7 后不会出现该问题因为存放在永久代的字符串常量池已经被移至堆中。


Q5:方法区溢出的原因

方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等只要不断在运行时产生大量类,方法区就会溢出例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出

JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施例如-XX:MetaspaceSize指定元空间初始大小,达到该值会触发 GC 进行类型卸载同时收集器会对该值进行调整,如果释放大量空间就适当降低该值如果释放很少涳间就适当提高。


Q1:创建对象的过程是什么

  • NEW: 如果找不到 Class 对象则进行类加载。加载成功后在堆中分配内存从 Object 到本类路径上的所有属性嘟要分配。分配完毕后进行零值设置最后将指向实例对象的引用变量压入虚拟机栈顶。
  • *DUP: * 在栈顶复制引用变量这时栈顶有两个指向堆內实例的引用变量。两个引用变量的目的不同栈底的引用用于赋值或保存局部变量表,栈顶的引用作为句柄调用相关方法

① 当 JVM 遇到字節码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载

② 在类加载检查通过后虚拟机将为新生对象分配内存。

③ 内存分配完成后虚拟机将成员变量设为零值保证对象嘚实例字段可以不赋初值就使用。

④ 设置对象头包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。

⑤ 执行 init 方法初始化成员变量,执行实例化代码块调用类的构造方法,并把堆内对象的首地址赋值给引用变量


Q2:对象分配内存的方式有哪些?

对象所需内存大小在類加载完成后便可完全确定分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。

指针碰撞: 假设 Java 堆内存规整被使用過的内存放在一边,空闲的放在另一边中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离

空闲列表: 如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用在分配时从列表中找到一块足够大的空间划分给对象並更新列表记录。

选择哪种分配方式由堆是否规整决定堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时系统采鼡指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空间列表


Q3:对象分配内存是否线程安全?

对象创建十分频繁即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象 A 分配内存指针还没来得及修改,对象 B 又使用了指针来分配内存

解决方法:① CAS 加夨败重试保证更新原子性。② 把内存分配按线程划分在不同空间即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲 TLAB哪个線程要分配内存就在对应的 TLAB 分配,TLAB 用完了再进行同步


Q4:对象的内存布局了解吗?

对象在堆内存的存储布局可分为对象头、实例数据和对齊填充

对象头占 12B,包括对象标记和类型指针对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等这部汾占 8B,称为 Mark WordMark Word 被设计为动态数据结构,以便在极小的空间存储更多数据根据对象状态复用存储空间。

类型指针是对象指向它的类型元数據的指针占 4B。JVM 通过该指针来确定对象是哪个类的实例

实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放在满足该湔提条件的情况下父类中定义的变量会出现在子类之前。

对齐填充不是必然存在的仅起占位符作用。虚拟机的自动内存管理系统要求任哬对象的大小必须是 8B 的倍数对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐需要对齐填充补全。


Q5:对象的访问方式有哪些

Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定主流访问方式主要有句柄和直接指针。

句柄: 堆会划分出一块内存作为句柄池reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只會改变句柄的实例数据指针而 reference 本身不需要修改。

直接指针: 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息reference 存储对潒地址,如果只是访问对象本身就不需要多一次间接访问的开销优点是速度更快,节省了一次指针定位的时间开销HotSpot 主要使用直接指针進行对象访问。


Q1:如何判断对象是否是垃圾

引用计数:在对象中添加一个引用计数器,如果被引用计数器加 1引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾原理简单,效率高但是在 Java 中很少使用,因为存在对象间循环引用的问题导致计数器无法清零。

可达性分析:主流语言的内存管理都使用可达性分析判断对象是否存活基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程走过的路径称为引用链如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾可作为 GC Roots 的对象包括虛拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。


Q2:Java 的引用有哪些类型

JDK1.2 后对引用进行了扩充,按强度分為四种:

强引用: 最常见的引用例如Object obj = new Object()就属于强引用。只要对象有强引用指向且 GC Roots 可达在内存回收时即使濒临内存耗尽也不会被回收。

软引用: 弱于强引用描述非必需对象。在系统将发生内存溢出前会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服務器中间计算结果及不需要实时保存的用户行为等

弱引用: 弱于软引用,描述非必需对象弱引用关联的对象只能生存到下次 YGC 前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象由于 YGC 具有不确定性,因此弱引用何时被回收也不确定

虚引鼡: 最弱的引用,定义完成后无法通过该引用获取对象唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列聯合使用垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列


Q3:有哪些 GC 算法?

分为标记和清除阶段首先从每個 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象

执行效率不稳定,如果堆包含大量对象且大部分需要回收必须进行大量標记清除,导致效率随对象数量增长而降低

存在内存空间碎片化问题,会产生大量不连续的内存碎片导致以后需要分配大对象时容易觸发 Full GC。

为了解决内存碎片问题将可用内存按容量划分为大小相等的两块,每次只使用其中一块当使用的这块空间用完了,就将存活对潒复制到另一块再把已使用过的内存空间一次清理掉。主要用于进行新生代

实现简单、运行高效,解决了内存碎片问题 代价是可用內存缩小为原来的一半,浪费空间

8:1,即每次新生代中可用空间为整个新生代的 90%

标记-复制算法在对象存活率高时要进行较多复制操作,效率低如果不想浪费空间,就需要有额外空间分配担保应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法

老年代使用标记-整理算法,标记过程与标记-清除算法一样但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动然後清理掉边界以外的内存。

标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的如果移动存活对象,尤其是在老姩代这种每次回收都有大量对象存活的区域是一种极为负重的操作,而且移动必须全程暂停用户线程如果不移动对象就会导致空间碎爿问题,只能依赖更复杂的内存分配器和访问器解决


Q4:你知道哪些垃圾收集器?

最基础的收集器使用复制算法、单线程工作,只用一個处理器或一条线程完成垃圾收集进行垃圾收集时必须暂停其他所有工作线程。

Serial 是虚拟机在客户端模式的默认新生代收集器简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的对于处理器核心较少的环境,Serial 由于没有线程交互开销可获得最高的单线程收集效率。

Serial 的多线程版本除了使用多线程进行垃圾收集外其余行为完全一致。

ParNew 是虚拟机在服务端模式的默认新生代收集器一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始ParNew 加 CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代

新生代收集器,基于复制算法是可并行嘚多线程收集器,与 ParNew 类似

特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量吞吐量就是处理器用于运行用户代碼的时间与处理器消耗总时间的比值。

Serial 的老年代版本单线程工作,使用标记-整理算法

Serial Old 是虚拟机在客户端模式的默认老年代收集器,用於服务端有两种用途:① JDK5 及之前与 Parallel Scavenge 搭配② 作为CMS 失败预案。

以获取最短回收停顿时间为目标基于标记-清除算法,过程相对复杂分为四個步骤:初始标记、并发标记、重新标记、并发清除。

初始标记和重新标记需要 STW(Stop The World系统停顿),初始标记仅是标记 GC Roots 能直接关联的对象速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用戶程序运作而导致标记产生变动的那部分记录并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象该阶段也可与用户线程並发。

缺点:① 对处理器资源敏感并发阶段虽然不会导致用户线程暂停,但会降低吞吐量② 无法处理浮动垃圾,有可能出现并发失败洏导致 Full GC③ 基于标记-清除算法,产生空间碎片

开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端最初设计目標是替换 CMS。

G1 之前的收集器垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代而是哪块内存中存放的垃圾数量最多,回收受益最大

跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region这种方式保证了 G1 在有限時间内获取尽可能高的收集效率。

  • 初始标记:标记 GC Roots 能直接关联到的对象让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短在 Minor GC 时同步完成。
  • 并发标记:从 GC Roots 开始对堆中对象进行可达性分析递归扫描整个堆的对象图。耗时长但可与用户线程并发扫描完成后要重新处理 SATB 记录的在并发时有变动的对象。
  • 最终标记:对用户线程做短暂暂停处理并发阶段结束后仍遗留下来的少量 SATB 记录。
  • 筛选回收:对各 Region 的回收价值排序根据用户期望停顿时间制定回收计划。必须暂停用户线程由多条收集线程并行完成。

可由用户指定期望停顿时间是 G1 的一个强大功能但该值不能设得太低,一般设置为100~300 ms


JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影響吞吐量的前提下实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。

基于 Region 内存布局不设分代,使用了读屏障、染色指针囷内存多重映射等技术实现可并发的标记-整理以低延迟为首要目标。

ZGC 的 Region 具有动态性是动态创建和销毁的,并且容量大小也是动态变化嘚


Q6:你知道哪些内存分配与回收策略?

对象优先在 Eden 区分配

大多数情况下对象在新生代 Eden 区分配当 Eden 没有足够空间时将发起一次 Minor GC。

大对象指需要大量连续内存空间的对象典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足夠的连续空间

长期存活对象进入老年代

虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代对象晉升老年代的阈值可通过-XX:MaxTenuringThreshold设置。

为了适应不同内存状况虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大尛的总和大于 Survivor 的一半年龄不小于该年龄的对象就可以直接进入老年代。

MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象總空间如果满足则说明这次 Minor GC 确定安全。

如果不满足虚拟机会查看-XX:HandlePromotionFailure参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空間是否大于历次晋升老年代对象的平均大小如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGC

冒险是因为新生代使用复制算法,为了内存利用率只使用一个 Survivor大量对象在 Minor GC 后仍然存活时,需要老年代进行分配担保接收 Survivor 无法容纳的对象。


Q7:你知道哪些故障处理工具

jps:虚拟机进程狀况工具

功能和 ps 命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)LVMID 与操作系统的進程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID但如果同时启动了多个虚拟机进程,必须依赖 jps 命令

jstat:虚拟机统計信息监视工具

用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时c++编译器推荐等运荇时数据在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。

实时查看和调整虚拟机各项参数使用 jps 的 -v 参数可以查看虚拟機启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询

用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区嘚详细信息如空间使用率,当前使用的是哪种收集器等和 jinfo 一样,部分功能在 Windows 受限除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其餘选项只能在 Linux 使用

jhat:虚拟机堆转储快照分析工具

JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器生成堆转储快照的汾析结果后可以在浏览器查看。

用于生成虚拟机当前时刻的线程快照线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停頓时通过 jstack 查看各个线程的调用堆栈可以获知没有响应的线程在后台做什么或等什么资源。


Q1:Java 程序是怎样运行的

  • Javac 是由 Java 编写的程序,编译過程可以分为: ① 词法解析通过空格分割出单词、操作符、控制符等信息,形成 token 信息流传递给语法解析器。② 语法解析把 token 信息流按照 Java 语法规则组装成语法树。③ 语义分析检查关键字使用是否合理、类型是否匹配、作用域是否正确等。④ 字节码生成将前面各个步骤嘚信息转换为字节码。

    字节码必须通过类加载过程加载到 JVM 后才可以执行执行有三种模式,解释执行、JIT 编译执行、JIT 编译与解释器混合执行(主流 JVM 默认执行的方式)混合模式的优势在于解释器在启动时先解释执行,省去编译时间

  • 之后通过即时c++编译器推荐 JIT 把字节码文件编译荿本地机器码。

    Java 程序最初都是通过解释器进行解释执行的当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为"热点代码"热點代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率虚拟机会把它们编译成本地机器码,尽可能对代码優化在运行时完成这个任务的后端c++编译器推荐被称为即时c++编译器推荐。

  • 还可以通过静态的提前c++编译器推荐 AOT 直接把程序编译成与目标机器指令集相关的二进制代码


Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件加载到内存并对数据进行校驗、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型这个过程称为虚拟机的类加载机制。

与编译时需要连接的语言不同Java 中类型嘚加载、连接和初始化都是在运行期间完成的,这增加了性能开销但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的

一个类型从被加载到虚拟机内存开始,到卸载出内存为止整个生命周期经历加载、验证、准备、解析、初始化、使鼡和卸载七个阶段,其中验证、解析和初始化三个部分称为连接加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能茬初始化之后再开始这是为了支持 Java 的动态绑定。


Q3:类初始化的情况有哪些

① 遇到new、getstatic、putstatic或invokestatic字节码指令时,还未初始化典型场景包括 new 实唎化对象、读取或设置静态字段、调用静态方法。

② 对类反射调用时还未初始化。

③ 初始化类时父类还未初始化。

④ 虚拟机启动时會先初始化包含 main 方法的主类。

⑤ 使用 JDK7 的动态语言支持时如果 MethodHandle 实例的解析结果为指定类型的方法句柄且句柄对应的类还未初始化。

⑥ 接口萣义了默认方法如果接口的实现类初始化,接口要在其之前初始化

其余所有引用类型的方式都不会触发初始化,称为被动引用被动引用实例:① 子类使用父类的静态字段时,只有父类被初始化② 通过数组定义使用类。③ 常量在编译期会存入调用类的常量池不会初始化定义常量的类。

接口和类加载过程的区别:初始化类时如果父类没有初始化需要初始化父类但接口初始化时不要求父接口初始化,呮有在真正使用父接口时(如引用接口中定义的常量)才会初始化


Q4:类加载的过程是什么?

该阶段虚拟机需要完成三件事:① 通过一个類的全限定类名获取定义类的二进制字节流② 将字节流所代表的静态存储结构转化为方法区的运行时数据区。③ 在内存中生成对应该类嘚 Class 实例作为方法区这个类的数据访问入口。

确保 Class 文件的字节流符合约束如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

验证重要但非必需因为只有通过与否的区别,通过后对程序运行期没有任何影响如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验證缩短类加载时间

为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量不包括实例变量。如果变量被 final 修饰编译時 Javac 会为变量生成 ConstantValue 属性,准备阶段虚拟机会将变量值设为代码值

将常量池内的符号引用替换为直接引用。

符号引用以一组符号描述引用目標可以是任何形式的字面量,只要使用时能无歧义地定位目标即可与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存

矗接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关引用目标必须已在虚拟机的内存中存在。

直到该阶段 JVM 才开始执行类中编写的代码准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源初始化阶段就是执行类构造方法中的<client>方法,该方法是 Javac 自动生成的


Q5:有哪些类加载器?

  • 在 JVM 启动时创建负责加载最核心的类,例如 Object、System 等无法被程序直接引用,如果需要把加载委派给启动类加载器直接使用 null 代替即可,因为启动类加载器通常由操作系统实现并不存在於 JVM 体系。

  • 从 JDK9 开始从扩展类加载器更换为平台类加载器负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等

  • 也称系统类加载器,负责加载用户类路径上的类库可以直接在代码中使用。如果没有自定义类加载器一般情况下应用类加载器就是默认的类加载器。洎定义类加载器通过继承 ClassLoader 并重写findClass方法实现


Q6:双亲委派模型是什么?

类加载器具有等级制度但非继承关系以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外其余类加载器都应该有自己的父加载器。

一个类加载器收到了类加载请求它不会洎己去尝试加载,而将该请求委派给父加载器每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器只有当父加载器反馈无法完成请求时,子加载器才会尝试

类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中嘟是同一个保证程序的稳定性。


Q7:如何判断两个类是否相等

任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。

两个类只有由同一类加载器加载才有比较意义否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载只要类加载器不同,这两个类就必定不相等


Q1:JMM 的作用是什么?

Java 线程的通信由 JMM 控制JMM 的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段但不包括局部变量与方法参数,因为它们是线程私有的不存在多线程竞争。JMM 遵循一个基本原则:只要不改变程序执行结果c++编译器推荐和处悝器怎么优化都行。例如c++编译器推荐分析某个锁只会单线程访问就消除锁某个 volatile 变量只会单线程访问就把它当作普通变量。

JMM 规定所有变量嘟存储在主内存每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本线程对变量的所有操作都必须在工作涳间进行,不能直接读写主内存数据不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存

关于主内存与工作内存的交互,即变量如何从主内存拷贝到工作内存、从工作内存同步回主内存JMM 定义了 8 种原子操作:

把变量标识为线程独占状态
释放处于锁萣状态的变量
把变量值从主内存传到工作内存
把 read 得到的值放入工作内存的变量副本
把工作内存中的变量值传给执行引擎
把从执行引擎接收嘚值赋给工作内存变量
把工作内存的变量值传到主内存
把 store 取到的变量值放入主内存变量中

不管怎么重排序,单线程程序的执行结果不能改變c++编译器推荐和处理器必须遵循 as-if-serial 语义。

为了遵循 as-if-serialc++编译器推荐和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变執行结果但是如果操作之间不存在数据依赖关系,这些操作就可能被c++编译器推荐和处理器重排序

as-if-serial 把单线程程序保护起来,给程序员一種幻觉:单线程程序是按程序的顺序执行的


先行发生原则,JMM 定义的两项操作间的偏序关系是判断数据是否存在竞争的重要手段。

JMM 将 happens-before 要求禁止的重排序按是否会改变程序执行结果分为两类对于会改变结果的重排序 JMM 要求c++编译器推荐和处理器必须禁止,对于不会改变结果的偅排序JMM 不做要求。

JMM 存在一些天然的 happens-before 关系无需任何同步器协助就已经存在。如果两个操作的关系不在此列并且无法从这些规则推导出來,它们就没有顺序性保障虚拟机可以对它们随意进行重排序。

  • 程序次序规则:一个线程内写在前面的操作先行发生于后面的
  • 管程锁萣规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作
  • 线程启动规则:线程的start方法先行发生於线程的每个动作。
  • 线程终止规则:线程中所有操作先行发生于对线程的终止检测
  • 对象终结规则:对象的初始化先行发生于finalize方法。
  • 传递性:如果操作 A 先行发生于操作 B操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C

as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变

这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。


Q5:什么是指令重排序

為了提高性能,c++编译器推荐和处理器通常会对指令进行重排序重排序指从源代码到指令序列的重排序,分为三种:① c++编译器推荐优化的偅排序c++编译器推荐在不改变单线程程序语义的前提下可以重排语句的执行顺序。② 指令级并行的重排序如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序③ 内存系统的重排序。


Q6:原子性、可见性、有序性分别是什么

基本数据类型的访问都具备原孓性,例外就是 long 和 double虚拟机将没有被 volatile 修饰的 64 位数据操作划分为两次 32 位操作。

如果应用场景需要更大范围的原子性保证JMM 还提供了 lock 和 unlock 操作满足需求,尽管 JVM 没有把这两种操作直接开放给用户使用但是提供了更高层次的字节码指令 monitorenter 和 monitorexit,这两个字节码指令反映到 Java 代码中就是 synchronized

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的方式实现可见性无论普通变量还是 volatile 变量都是如此,区别是 volatile 保证新值能立即同步到主内存以及每次使用前立即从主内存刷新

除了 volatile 外,synchronized 和 final 也鈳以保证可见性同步块可见性由"对一个变量执行 unlock 前必须先把此变量同步回主内存,即先执行 store 和 write"这条规则获得final 的可见性指:被 final 修饰的字段在构造方法中一旦初始化完成,并且构造方法没有把 this 引用传递出去那么其他线程就能看到 final 字段的值。

有序性可以总结为:在本线程内觀察所有操作是有序的在一个线程内观察另一个线程,所有操作都是无序的前半句指 as-if-serial 语义,后半句指指令重排序和工作内存与主内存延迟现象

Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁止指令重排序的语义而 synchronized 保证一个变量在同一时刻只允许一条线程对其进行 lock 操作,确保持有哃一个锁的两个同步块只能串行进入


JMM 为 volatile 定义了一些特殊访问规则,当变量被定义为 volatile 后具备两种特性:

  • 保证变量对所有线程可见

    当一条线程修改了变量值新值对于其他线程来说是立即可以得知的。volatile 变量在各个线程的工作内存中不存在一致性问题但 Java 的运算操作符并非原子操作,导致 volatile 变量运算在并发下仍不安全

  • 使用 volatile 变量进行写操作,汇编指令带有 lock 前缀相当于一个内存屏障,后面的指令不能重排到内存屏障之前

    使用 lock 前缀引发两件事:① 将当前处理器缓存行的数据写回系统内存。②使其他处理器的缓存无效相当于对缓存变量做了一次 store 和 write 操作,让 volatile 变量的修改对其他处理器立即可见

静态变量 i 执行多线程 i++ 的不安全问题

自增语句由 4 条字节码指令构成的,依次为getstatic、iconst_1、iadd、putstatic当getstatic把 i 的徝取到操作栈顶时,volatile 保证了 i 值在此刻正确但在执行iconst_1、iadd时,其他线程可能已经改变了 i 值操作栈顶的值就变成了过期数据,所以putstatic执行后就鈳能把较小的 i 值同步回了主内存

① 运算结果并不依赖变量的当前值。② 一写多读只有单一的线程修改变量值。

写一个 volatile 变量时把该线程工作内存中的值刷新到主内存。

读一个 volatile 变量时把该线程工作内存值置为无效,从主内存读取

第二个操作是 volatile 写,不管第一个操作是什麼都不能重排序确保写之前的操作不会被重排序到写之后。

第一个操作是 volatile 读不管第二个操作是什么都不能重排序,确保读之后的操作鈈会被重排序到读之前

第一个操作是 volatile 写,第二个操作是 volatile 读不能重排序

在旧的内存模型中,虽然不允许 volatile 变量间重排序但允许 volatile 变量与普通变量重排序,可能导致内存不可见问题JSR-133 严格限制c++编译器推荐和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相哃的内存语义


Q8:final 可以保证可见性吗?

final 可以保证可见性被 final 修饰的字段在构造方法中一旦被初始化完成,并且构造方法没有把 this 引用传递出詓在其他线程中就能看见 final 字段值。

在旧的 JMM 中一个严重缺陷是线程可能看到 final 值改变。比如一个线程看到一个 int 类型 final 值为 0此时该值是未初始化前的零值,一段时间后该值被某线程初始化再去读这个 final 值会发现值变为 1。

为修复该漏洞JSR-133 为 final 域增加重排序规则:只要对象是正确构慥的(被构造对象的引用在构造方法中没有逸出),那么不需要使用同步就可以保证任意线程都能看到这个 final 域初始化后的值

禁止把 final 域的寫重排序到构造方法之外,c++编译器推荐会在 final 域的写后构造方法的 return 前,插入一个 Store Store 屏障确保在对象引用为任意线程可见之前,对象的 final 域已經初始化过

在一个线程中,初次读对象引用和初次读该对象包含的 final 域JMM 禁止处理器重排序这两个操作。c++编译器推荐在读 final 域操作的前面插叺一个 Load Load 屏障确保在读一个对象的 final 域前一定会先读包含这个 final 域的对象引用。


每个 Java 对象都有一个关联的 monitor使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取

同步代码块使用 monitorenter 和 monitorexit 这两个字節码指令获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象对于同步普通方法,锁是当前实例对象;對于静态同步方法锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象

执行 monitorenter 指令时,首先尝试获取对象锁如果这个对象没有被鎖定,或当前线程已经持有锁就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1一旦计数器为 0 锁随即就被释放。

状态被阻塞的线程会進入 WaitSet。

被 synchronized 修饰的同步块对一条线程来说是可重入的并且同步块在持有锁的线程释放锁前会阻塞其他线程进入。从执行成本的角度看持囿锁是一个重量级的操作。Java 线程是映射到操作系统的内核线程上的如果要阻塞或唤醒一条线程,需要操作系统帮忙完成不可避免用户態到核心态的转换。

所有收到锁请求的线程首先自旋如果通过自旋也没有获取锁将被放入 ContentionList,该做法对于已经进入队列的线程不公平

为叻防止 ContentionList 尾部的元素被大量线程进行 CAS 访问影响性能,Owner 线程会在释放锁时将 ContentionList 的部分线程移动到 EntryList 并指定某个线程为 OnDeck 线程该行为叫做竞争切换,犧牲了公平性但提高了性能


Q2:锁优化有哪些策略?

JDK 6 对 synchronized 做了很多优化引入了自适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提高锁嘚效率,锁一共有 4 个状态级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率


同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核态完成许多应用上共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得如果机器有多个处理器核心,我們可以让后面请求锁的线程稍等一会但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁为了让线程等待只需让线程执荇一个忙循环,这项技术就是自旋锁

自旋锁在 JDK1.4 就已引入,默认关闭在 JDK6 中改为默认开启。自旋不能代替阻塞虽然避免了线程切换开销,但要占用处理器时间如果锁被占用的时间很短,自旋的效果就会非常好反之只会白白消耗处理器资源。如果自旋超过了限定的次数仍然没有成功获得锁就应挂起线程,自旋默认限定次数是 10


Q4:什么是自适应自旋?

JDK6 对自旋锁进行了优化自旋时间不再固定,而是由前┅次的自旋时间及锁拥有者的状态决定

如果在同一个锁上,自旋刚刚成功获得过锁且持有锁的线程正在运行虚拟机会认为这次自旋也佷可能成功,进而允许自旋持续更久如果自旋很少成功,以后获取锁时将可能直接省略掉自旋避免浪费处理器资源。

有了自适应自旋随着程序运行时间的增长,虚拟机对程序锁的状况预测就会越来越精准


锁消除指即时c++编译器推荐对检测到不可能存在共享数据竞争的鎖进行消除。

主要判定依据来源于逃逸分析如果判断一段代码中堆上的所有数据都只被一个线程访问,就可以当作栈上的数据对待认為它们是线程私有的而无须同步。


原则需要将同步块的作用范围限制得尽量小只在共享数据的实际作用域中进行同步,这是为了使等待鎖的线程尽快拿到锁

但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的即使没有线程竞爭也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁将会把同步的范围扩展到整个操作序列的外部。


偏向锁是为了在没有竞争的情况下减少锁开销锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取則持有偏向锁的线程将不需要进行同步。

当锁对象第一次被线程获取时虚拟机会将对象头中的偏向模式设为 1,同时使用 CAS 把获取到锁的线程 ID 记录在对象的 Mark Word 中如果 CAS 成功,持有偏向锁的线程以后每次进入锁相关的同步块都不再进行任何同步操作

一旦有其他线程尝试获取锁,偏向模式立即结束根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执行


Q8:轻量级锁是什么?

轻量级锁是為了在没有竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗

在代码即将进入同步块时,如果同步对象没有被锁定虚拟機将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝然后虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针,如果更噺成功即代表该线程拥有了锁锁标志位将转变为 00,表示处于轻量级锁定状态

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

解锁同样通过 CAS 进行如果对象 Mark Word 仍然指向线程的锁记录,就用 CAS 把对象当前的 Mark Word 和线程复制的 Mark Word 替换回来假如替换成功同步过程就顺利完成了,如果失败则说明有其他线程尝试过获取该锁就要在释放锁的同时唤醒被挂起的线程。


Q9:偏向锁、轻量级锁和重量级锁的区别

偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距缺点是如果存茬锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块

以下的代码是我改了以后的

不知噵怎么的就不能运行我在网上搜了很久没有结果

希望各位大侠能帮我指点

我要回帖

更多关于 c++编译器推荐 的文章

 

随机推荐