posted in java 

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载 7个阶段。

验证、准备、解析3个部分统称为连接。


加载

加载时类加载过程中的一个阶段,虚拟机需要完成3件事情

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流锁代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证

验证时连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃。

验证阶段大致上会完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 文件格式验证

    • 是否已魔数0xCAFEBABE开头
    • 主次版本号是否在当前虚拟机处理范围之内
    • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
    • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
    • ...
  2. 元数据验证

    • 这个类是否有父类
    • 这个类的父类是否继承了不允许被继承的类
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    • 类中的字段、方法是否与父类产生矛盾
    • ...
  3. 字节码验证

  4. 符号引用验证

posted in java 

Class文件对应着唯一一个类或接口的定义信息,但是类或接口并不一定都得定义在文件里(也可以通过类加载器直接生成)。

Class文件是一组以8位字节为基础单位的二进制流。

Java虚拟机规范中规定 ,Class文件格式采用一种类似于C语言结构体的结构来存储数据,有两种数据类型:无符号数和表。

  • 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1、2、4、8 个字节。
  • 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。

表是有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以'_info'结尾。用于描述有层次关系的符合结构的数据。

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

第1行,u4 = 4个字节,前4个字节是魔数,CAFEBABE,每个class文件都是以这个4个字节开始的;
第2行,u2 = 2个字节,代表的是次版本号;
第3行,u2 = 2个字节,代表的是主版本号;
第4行,u2 = 2个字节,constant_pool_conunt 代表的是常量池的容量,常量个数;
第5行,cp_info,constant_pool,代表的是常量池中每个常量的信息,常量个数从1开始到constant_pool_conunt - 1;
第6行,u2 = 2个字节,表示的是access_flags访问标志;
第7行,2个字节,表示的是this_class,类索引;
第8行,2个字节,表示的是super_class,父类索引;
第9行,2个字节,接口索引个数
第10行,接口索引集合
第11行,fields_count,字段集合个数
第12行,fields,字段集合
第13行,2个字节,methods_count,方法个数
第14行,方法集合
第15行,属性表集合个数
第16行,属性表集合信息

分析字节码文件

这是一个字节码文件

CA FE BA BE 00 00 00 34  00 1D 0A 00 05 00 18 09
00 04 00 19 09 00 04 00  1A 07 00 1B 07 00 1C 01
00 04 6E 61 6D 65 01 00  12 4C 6A 61 76 61 2F 6C
61 6E 67 2F 53 74 72 69  6E 67 3B 01 00 03 61 67
65 01 00 01 49 01 00 06  3C 69 6E 69 74 3E 01 00
03 28 29 56 01 00 04 43  6F 64 65 01 00 0F 4C 69
6E 65 4E 75 6D 62 65 72  54 61 62 6C 65 01 00 07
67 65 74 4E 61 6D 65 01  00 14 28 29 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53  74 72 69 6E 67 3B 01 00
07 73 65 74 4E 61 6D 65  01 00 15 28 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53  74 72 69 6E 67 3B 29 56
01 00 06 67 65 74 41 67  65 01 00 03 28 29 49 01
00 06 73 65 74 41 67 65  01 00 04 28 49 29 56 01
00 0A 53 6F 75 72 63 65  46 69 6C 65 01 00 0B 50
65 72 73 6F 6E 2E 6A 61  76 61 0C 00 0A 00 0B 0C
00 06 00 07 0C 00 08 00  09 01 00 10 63 6F 6D 2F
68 69 74 6F 6C 2F 50 65  72 73 6F 6E 01 00 10 6A
61 76 61 2F 6C 61 6E 67  2F 4F 62 6A 65 63 74 00
21 00 04 00 05 00 00 00  02 00 02 00 06 00 07 00
00 00 02 00 08 00 09 00  00 00 05 00 01 00 0A 00
0B 00 01 00 0C 00 00 00  1D 00 01 00 01 00 00 00
05 2A B7 00 01 B1 00 00  00 01 00 0D 00 00 00 06
00 01 00 00 00 03 00 01  00 0E 00 0F 00 01 00 0C
00 00 00 1D 00 01 00 01  00 00 00 05 2A B4 00 02
B0 00 00 00 01 00 0D 00  00 00 06 00 01 00 00 00
09 00 01 00 10 00 11 00  01 00 0C 00 00 00 22 00
02 00 02 00 00 00 06 2A  2B B5 00 02 B1 00 00 00
01 00 0D 00 00 00 0A 00  02 00 00 00 0D 00 05 00
0E 00 01 00 12 00 13 00  01 00 0C 00 00 00 1D 00
01 00 01 00 00 00 05 2A  B4 00 03 AC 00 00 00 01
00 0D 00 00 00 06 00 01  00 00 00 11 00 01 00 14
00 15 00 01 00 0C 00 00  00 22 00 02 00 02 00 00
00 06 2A 1B B5 00 03 B1  00 00 00 01 00 0D 00 00
00 0A 00 02 00 00 00 15  00 05 00 16 00 01 00 16
00 00 00 02 00 17 

每个class文件的头4个字节 CA FE BA BE 称为魔数,也就是上面的图class文件格式中第一个,u4 magic。它的唯一作用 是确定这个文件是否 为一个能被虚拟机接受的class文件。

下两个字节 00 00 代表的是 次版本号, 十进制数 = 0
下两个字节 00 34 代表的是 主版本号,十进制数=52

主次版本号表示的是jdk的版本,52对应的是1.8, 51对应的是1.7

之前遇到的一个错误,编译SpringBoot代码的时候用的1.8,在1.7的java环境中执行的话,就会出现这个52的错误。

常量池及常量

下两个字节 00 1D 代表的是 常量池中常量的个数,
常量个数从1开始的,0x1D = 29,最终结果是28,索引是1--28
第0号常量池被JVM占用,表示的是什么都不引用。

常量分类
字面量类型,比较接近于Java语言层面的常量概念,
符号引用类型,属于编译原理方面的概念
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符

Java代码在编译的时候,是动态链接的,也就是说在Class文件中不会保存各个方法、字段的最终内存信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。
虚拟机运行的时候,需要从常量池获得对应的符号引用,在类创建或运行时解析、翻译到具体的内存地址中。

常量池中每项常量都是一个表,表开始的第一位是一个u1类型的标志位,tag,代表当前这个常量属于哪种类型常量。

每个常量的结构类似这样的

CONSTANT_Class_info{
    u1 : tag = 7
    u2 : name_index 
}


第一个常量
第一个字节 0A
0x0A = 10
在常量池项目类型中,标志位为10的是CONSTANT_Method_info,表示类中 方法的符号引用
后面还有4个字节,前两个字节表示是执行声明字段的类或接口描述符CONSTANT_Class_info的索引项
后两个字节表示的是指向字段描述符CONSTANT_NamaeAndType的索引项。
前两个字节 00 05
0x00 05 = CONSTANT_Fieldref_info5 ,表示引用的是第5个常量
后两个字节
0x00 18 = 24,表示引用的是第24个常量
索引为5的常量代表的是个类,索引为24的常量代表的是个方法名,也就是说这个方法引用的是 #5.#24

常量池中第一个常量:
// #1 = Methodref #5.#24 // java/lang/Object."":()V

()V 表示的是方法 没有入参,没有返回值


第二个常量
第一个字节 09
0x09 = 9
表示的是 CONSTANT_Fieldref_info,其中的结构是 u1 u2 u2
第一个字节就是标志位9
后面4个字节中,前两个字节表示声明字段的类或者接口描述符CONSTANT_Class_info的索引项
后两个字节表示指向字段描述符CONSTANT_NameAndType的索引项

前两个字节
0x00 04 = 4,表示引用常量池中第4个常量,这个常量代表的是一个类

后两个字节
0x00 19 = 25,表示引用常量池中第25个常量,这个常量代表一个属性名。

常量池中第二个常量:
// #2 = Fieldref #4.#25 // com/hitol/Person.name:Ljava/lang/String;

代表的就是 Person类中name属性。


第三个常量

09 = CONSTANT_Fieldref_info

0x00 04 = 4
0x00 1A = 26

// #4.#26

// #3 = Fieldref #4.#26 // com/hitol/Person.age:I


第四个常量

07 = CONSTANT_Class_info
0x00 1B = 27

// #27

// #4 = Class #27 // com/hitol/Person


第五个常量

07 = CONSTANT_Class_info

0x00 1C = = 28

// #28
// #5 = Class #28 // java/lang/Object


第六个常量

01 = CONSTANT_Utf8_info
length = 0x00 04 = 4,长度为4
bytes = 长度为4的字符串,6E 61 6D 65

http://www.ab126.com/goju/1711.html
在线转换后,

6E 61 6D 65 转换后表示的字符串是name.

// #6 = Utf8 name


第七个常量
01 = CONSTANT_Utf8_info
length = 0x00 12 = 18
bytes = 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B

转换后是 L j a v a / l a n g / S t r i n g ;

顺便提一下,Java中定义的变量或方法名,最大长度就是lenght的最大值,两个字节能表示的最大值就是65535。


第八个常量
01 = CONSTANT_Utf8_info
length = 3
bytes = a g e


第九个常量
01 = CONSTANT_Utf8_info
length = 1
bytes = I


第十个常量
01 = CONSTANT_Utf8_info
length = 6
bytes = < i n i t >


第十一个
01 = CONSTANT_Utf8_info
length =3
bytes = ( ) V


第十二个
01 = CONSTANT_Utf8_info
length =4
bytes =C o d e


第十三个
01 = CONSTANT_Utf8_info
length = 16
bytes =L i n e N u m b e r T a b l e


第十四个变量
01 = CONSTANT_Utf8_info
length = 07
bytes =67 65 74 4E 61 6D 65 = getName


第十五个变量
01 = CONSTANT_Utf8_info
length = 20
bytes = 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B =( ) L j a va / l a n g / S t r i n g ;

第十六个
01 = CONSTANT_Utf8_info
length = 07
bytes = 73 65 74 4E 61 6D 65 = s e t N a m e


第十七个
01 = CONSTANT_Utf8_info
length = 07
bytes = 28 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 = ( L j a v
a / l a n g / S t r i n g ; ) V

第十八个
01 = CONSTANT_Utf8_info
length = 06
bytes = 67 65 74 41 67 65 = g e t A g e


第十九个
01 = CONSTANT_Utf8_info
length = 03
bytes = 28 29 49 = ( ) I

第二十个
01 = CONSTANT_Utf8_info
length = 06
bytes = 73 65 74 41 67 65 = s e t A g e

第二十一个
01 = CONSTANT_Utf8_info
length = 04
bytes = 28 49 29 56 = ( I ) V

第二十二个
01 = CONSTANT_Utf8_info
length = 0A =10
bytes = 53 6F 75 72 63 65 46 69 6C 65 = S o u r c e F i l e

第二十三个
01 = CONSTANT_Utf8_info
length = 0B = 11
bytes = 50 65 72 73 6F 6E 2E 6A 61 76 61 = P e r s o n . j a v a

第二十四个
tag = 12 代表的是 CONSTANT_NameAndType_info
该结构还有4个字节,前两个字节表示指向该字段或方法名称常量项的索引,后两个字节表示指向该字段或方法描述符常量项的索引。

前两个字节:00 0A = 10
后两个字节:00 0B = 11
// #24 = #10:#11

第二十五个
tag = 0C = 12 = CONSTANT_NameAndType_info
前两个字节:00 06 = 6
后两个字节:00 07 = 7
// #25 = #6:#7

第二十六个
tag = 0C = CONSTANT_NameAndType_info
00 08
00 09
// #26 = #8:#9

第二十七个
tag = 01 = CONSTANT_Utf8_info
length = 10 = 16
bytes = 63 6F 6D 2F 68 69 74 6F 6C 2F 50 65 72 73 6F 6E = c o m / h i t o l / P e r s o n

第二十八个
tag = 01 = CONSTANT_Utf8_info
length = 10 = 16
bytes = 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 = j a v a / l a n g / O b j e c t


access_flags

常量池中28个常量已经分析完成,根据class文件格式表中的类型,下面两个字节表示access_flags。

访问标志

0x00 21 = ACC_PUBLIC + ACC_SUPER
ACC_PUBLIC 、 ACC_SUPER转换为2进制后执行|运算,

   000001
|  100000
----------
   100001

100001对应的16进制就是21。


类索引、父类索引、接口索引集合

类索引用于确定本类的全限定名、
父类索引用于切丁父类的全限定名。
由于Java单继承的,所以父类索引只有一个。

类索引的结构是CONSTANT_Class_info
类索引00 04,常量池中第4个常量,
父类索引00 05,常量池中第5个常量。

接口索引集合就是用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句的接口顺序从左到右排列在接口索引集合中。
接口索引集合 00 00 表示个数为0

字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

可以包括的信息有:

  1. 字段的作用域
    public\private\protected修饰符

  2. 是实例变量还是类变量
    static修饰符

  3. 可变性
    final

  4. 并发可见性
    volatile修饰符,是否强制从主内存读写

  5. 可否被序列化
    transient修饰符

  6. 字段数据类型
    基本类型、对象、数组

  7. 字段名称

字段表结构

字段修饰符在access_flags中,其访问标志为

access_flags后是两个索引,name_index 和 descriptor_index。他们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符,

第一个字段

根据图中所示,前两个字节表示access_flags。
0x00 02 = ACC_PRIVATE 表示这个字段是private修饰的。

接下来两个字节表示name_index,代表字段的简单名称。
00 06 表示常量池中第6个常量 = name。

接下来两个字节表示descriptior_index ,代表方法的描述符。
00 07 = 常量池中第7个常量= Ljava/lang/String;

简单名称是指没有类型和参数修饰的方法或者字段名称
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。L表示是一个对象,后面跟着对象的全限定名。

对于数组类型,每一维度将使用一个前置的'['字符描述,
例如:定义了一个String类型的二维数组
String [][] 将被记录为 “[[Ljava.lang.String;”

第一个字段就表示为 private String name;

字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。

在本例中是00 00 表示没有额外信息。


第二个字段
00 02 = ACC_PRIVATE 表示private

00 08 表示常量池中第8个常量 age
00 09 表示第9个常量 I
00 00 表示没有额外信息

I 表示是int类型。

方法表集合



下面两个字节00 05 表示的是方法的个数。


第一个方法

前两个字节 00 01 表示访问标志access_flags,表示public
接下来两个字节 00 0A 表示名称索引name_index,表示常量池中第10个常量,
接下来两个字节00 0B 表示描述符索引descriptor_index,表示常量池中第11个常量,()V
00 01 属性个数
00 0C = 12 Code

Code属性见下面属性表集合。

这个方法表示的是无参构造方法。


第二个方法
00 01 = public
00 0D = 第12个常量 = Code

Code属性见下面属性表集合


属性表集合

Code属性
Java程序方法中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。

Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,接口或抽象类中的方法就不存在Code属性。
如果有Code属性,其结构为

前两个字节表示的是attribute_name_index是一项指向CONSTANT_Utf8_info的常量的索引,常量值固定为“Code”,代表了该属性的属性名称,

接下来4个字节指示了属性值的长度,
接下来2个字节,max_stack,代表了操作数栈深度的最大值
接下来2个字节,max_locals,代表了局部变量表所需的存储空间。单位是Slot,Slot是虚拟机为局部变量分配内存使用的最小单位。
接下来4个字节,code_length,字节码长度,虽然是4个字节,理论上最大值232 - 1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际上只是用了u2的长度,如果超过这个限制,javac编译器也会拒绝编译。
接下来1个字节,code,用于存储字节码指令的一系列字节流,表示一个指令。字节码长度个字节,表示几条指令。
接下来2个字节,exception_table_length ,异常表长度
exception_info 异常表信息
异常表属性结构

包含4个字段,这些字段的含义为:如果当字节码在第start_pc行到第end_pc(不包含end_pc)行出现了类型为catch_type或者其子类的异常,则转到第handler_pc行继续处理。当catch_type为0时,代表任何异常情况都需要转向到handler_pc处理.

接下来2个字节,attributes_count
attributes_info

第一个方法中Code属性:
00 0C = Code
00 00 00 1D 属性值的长度为29
00 01 操作数栈深度最大值为1
00 01 局部变量表空间为1
00 00 00 05 code_length = 5,字节码长度是5

2A B7 00 01 B1

读入2A

0x2A = aload_0 指令,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。

读入B7

指令为
invokespecial,这条指令的作用是以栈顶的reference的数据所执行的对象作为方法接受者,调用此方法的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型的常量,即此方法的方法符号引用。

读入00 01
这个是上一条指令的参数,常量池中第一个常量,表示java/lang/Object."":()V,实例构造器init方法的符号引用。

读入B1

查表得对应的指令为return,含义是返回此方法,并且返回值为void,这条方法指令执行后,当前方法结束。

00 00 表示异常表长度为0


00 01 attributes_count = 1

接下来两个字节
00 0D = 13 = LineNumberTable

LineNumberTable
LineNumberTable,用于描述Java源码行号与字节码行号之间的对应关系,它并不是运行时必须的属性,但 默认会生成到Class文件之中,
主要应用就是,当程序抛出异常时,堆栈中显示的错误行号。


LocalVariableTable 局部变量表
JVM中,Java虚拟机栈执行的时候创建栈帧,用来存放局部变量表,就是这个东西。
用于描述栈帧中局部变量表的中的变量与Java源码中定义的变量之间的关系。


idea中有个插件 jclasslib ,可以直观、友好的查看class文件中的内容


使用javap输出常量表

javap -verbose XXX.class

  Last modified 2020-6-28; size 550 bytes
  MD5 checksum 0a4eff80df81adb2a2ee5ac6206f461c
  Compiled from "Person.java"
public class com.hitol.Person
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#25         // com/hitol/Person.name:Ljava/lang/String;
   #3 = Fieldref           #4.#26         // com/hitol/Person.age:I
   #4 = Class              #27            // com/hitol/Person
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               name
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               age
   #9 = Utf8               I
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               getName
  #15 = Utf8               ()Ljava/lang/String;
  #16 = Utf8               setName
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Utf8               getAge
  #19 = Utf8               ()I
  #20 = Utf8               setAge
  #21 = Utf8               (I)V
  #22 = Utf8               SourceFile
  #23 = Utf8               Person.java
  #24 = NameAndType        #10:#11        // "<init>":()V
  #25 = NameAndType        #6:#7          // name:Ljava/lang/String;
  #26 = NameAndType        #8:#9          // age:I
  #27 = Utf8               com/hitol/Person
  #28 = Utf8               java/lang/Object
{
  public com.hitol.Person();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 17: 0

  public void setAge(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #3                  // Field age:I
         5: return
      LineNumberTable:
        line 21: 0
        line 22: 5
}
SourceFile: "Person.java"

TODO

类的创建和动态连接

参考
《深入理解Java虚拟机》

Java Virtual Machine Specification

posted in java 

JVM内存模型
这个是JVM内存模型

Java线程内存模型JM
这个是Java线程内存模型。
Java内存模型是一种内存规范。

posted in java 

JVM运行时数据区

  • 程序计数器

    • 是一块很小的内存空间,可以看做是当前线程执行的行号指示器。字节码解释器工作时就是通过改变这个计数器的数值来选取下一条需要执行的字节码指令
    • 线程私有
    • 不会发生OOM
  • Java虚拟机栈

    • 线程私有
    • 为执行Java方法服务
    • 其工作原理类似于数据结构中的栈--FIFO。每个方法在执行的过程中会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 会发生OOM、StackOverflowError
  • 本地方法栈

    • 线程私有
    • 与虚拟机栈功能类似
    • 为Native方法服务
    • 会发生OOM、StackOverflowError
  • Java堆

    • 线程共享
    • 几乎所有的对象实例都在堆中分配。
    • 逃逸分析、标量替换-->栈上分配
    • 新生代:老年代 = 1:2

    • Eden:S0:S1 = 8:1:1

  • 方法区

    • 线程共享
    • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
    • 运行时常量池:用于存放编译期生成的各种字面量和符号引用
    • 会出现OOM

辣鸡回收算法与辣鸡回收器

辣鸡回收需要做的3件事情

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

上面JVM运行时数据区中线程私有变量(本地方法栈、JVM虚拟机栈、程序计数器)会随着方法执行的完成而自动释放内存,所以这部分的内存不需要考虑回收。需要考虑回收的内存主要就是堆内存与方法区,只有在程序运行的时候才知道会创建哪些对象,哪些需要回收。

判断对象是否存活

  1. 程序计数器

    给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就+1,当引用失效时,计数器的值-1.

    但是主流的Java虚拟机里面并没有使用这种方式,最主要的原因是它很难解决对象之间相互循环引用的问题。

  2. 可达性分析(GC Roots)

    使用最多的是这种方式。
    这种算法的思路就是,选择一个根节点,从这个节点往下搜索,搜索过程中走过的路径称为引用链,当一个对象到根节点没有引用链存在时,也就是不可达,这种对象就是辣鸡对象。
    哪些对象可以作为GC Roots

    • 虚拟机栈中引用的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中引用的对象
  3. 引用的类型

    • 强引用:new出来的对象都是强引用对象,不会被回收。
    • 软引用
    • 弱引用
    • 虚引用
  4. 对象的自我拯救-二次标记

    • 当一个对象被标记为辣鸡后并不会立马被回收掉。
    • 第一次经过可达性分析后发现该对象是辣鸡对象,会被第一次标记并且进行一次筛选,筛选的条件是此对象时候有必要执行finalize方法,
    • 如果该对象需要执行finallize方法,那么这个对象会被放置在一个F-Queue的队列中,稍后GC将会对队列中的对象信心小规模的二次标记。
    • 如果对象在执行finalize过程中,与GC Roots建立联系了,那么这个对象就不会被回收,反之就会被回收了。
  5. 回收方法区
    永久代主要回收两类对象:废弃常量和无用的类。
    废弃常量好理解,没有任何对象引用常量池中的某个变量,这个变量就是废弃常量。
    如何判断一个类是无用的类

    • 该类的所有实例都已经被回收掉
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

辣鸡回收算法

  1. 标记-清除算法
    当对象被标记为辣鸡后,直接清理掉。
    效率不高、容易产生内存碎片

  2. 标记-整理算法
    被标记为辣鸡的对象回收之后,整理内存空间。

  3. 复制算法
    将内存分为大小相等的两块,每次内存分配的时候只使用其中的一块,当这块内存满了只有,将存活对象复制到另一块内存空间中,然后整块清理。
    实现简单、运行高效
    但是浪费一半的内存。

  4. 分代收集算法
    目前使用这种算法。

新生代中发生GC -- minor gc

minor GC test

public class HeapTest {

    private byte[] bytes = new byte[1024*100];

    public static void main(String[] args) throws InterruptedException {
        List<HeapTest> list = new ArrayList<>();
        while (true) {
            System.out.println(111);
            list.add(new HeapTest());
            Thread.sleep(10);
        }
    }
}

minor-g

这图是上面的代码运行时的gc情况。

从图中可以得出结论,新生成的对象都是在Eden区,当Eden区满了后,会执行gc回收,将Eden区与S0区中的存活对象拷贝到S1,并清空Eden、S0,存活对象年龄+1;
当新生对象又一次将Eden区填满后,会将Eden区、S1中的存活对象拷贝到S0,并清空S1,存活对象年龄+1;
当存活对象的年龄达到15后,会被认为是老不死对象,挪到老年代内存中。
新生代中的GC 叫做minor GC。
老年代内存占满后进行的GC就是FULL GC。
minor GC正常情况下可以回收70--95%的空间。

对象进入老年代的情况:

  • 长期存活的对象进入老年代
    • 对象年龄达到15后
  • 内存担保机制
    • 当Eden区的内存不足以容纳新生成的对象的时候,新生成的对象会直接进入老年代
  • 动态年龄判断(大对象直接进入老年代)
    • 新生成的对象的内存大于Eden区内存的50%会直接进入老年代

JVM调优的目的:减少FULL GC

辣鸡收集器

  • Serial
  • ParNew
  • Parallel Scavenge
  • Serial Old
  • Parallel Old
  • CMS
  • G1

TODO
JVM调优参数
JDK工具
安全点与安全区域
STW
各种垃圾收集器
逃逸分析与标量替换