jvm学习(java se8)
1.各种jvm特点介绍(包含以前的):
- Sun Classic VM (jdk1.1 版本的虚拟机)
特点: 全世界第一个商用的虚拟机,只能使用纯解释器的方式来执行java代码
- Exact VM (jdk1.2 solari平台)
特点: 开始具备高性能vm的特点,有准确性内存管理,解释器和编译器混合使用以及二级即时编译特性,但是只在jdk1.2的solari平台使用过
- HotSpot VM (jdk1.3发布)
特点: 热点代码追踪技术等等
- KVM (j2ME 时代sun公司的产物)
特点:简单,轻便,就是比较慢
- JRockit VM (sun公司的产品,2008年呗Oracle收购之后就呗HotSpot整合了)
特点: 快,有垃圾收集器,MissionControl控制套件(用来控制生产环境内存泄漏的)
- J9 VM (IBM 为自家java程序开发的一个VM)
特点: 高度模块化,任意平台通用(SE,EE,ME),快
- DalVik (android 平台的VM)
特点: 性能好,快.只限android平台使用
2.JVM 的内存管理
jvm的内存管理分为两大部分,线程共享区和线程独占区,具体划分看下图:
接下来重点讲解几个重要的概念:
2.1动态常量池
存在于方法区中,常量都是不变的,为什么叫动态常量池呢,就是因为这个池可以动态的增加(String的intern()方法),看以下代码:
String a = "abc";
String b = "abc";
logger.info(a == b); // 1
String c = new String("abc");
logger.info(b == c); // 2
logger.info(b == c.intern()); // 3
在这里变量a和b就是一个常量,值存放在java的线程共享区的方法区中,引用存放在线程独享区中的虚拟机栈中,在方法区中存在一个StringTable相当于一个HashSet,只会存在一个“abc”实例,所以1对应的输出为true。
变量c的引用存放在线程独享区中的虚拟机栈中,值因为是一个变量,存放在线程共享区中的java堆中,所以和b变量的地址不同,2的对应输出为false。
当调用c.intern()方法时,jvm会将对应的值从java堆移动到方法区中,即动态常量池中。所以3对应的输出为true,对应的流程图如下:
2.2直接内存
java的NIO使用,javaNIO是一种面向缓冲的技术实现框架,其中缓冲的内存使用的不是java堆内存,使用的是直接内存,当然同样受限于物理机器的内存。
2.3对象在内存中的布局
对象的创建步骤:
变量包括数据头(header)和变量值(instanceData)和填充(padding),数据头包含了变量的各种运行时数据(哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳…)和类型指针。变量值就是变量的值,填充是由于数据的大小只能为8的倍数,当数据大小不为8的倍数时,就会进行填充位数。
3. 垃圾回收
3.1 在哪里进行垃圾回收?
大部分是线程共享区的java堆,小部分是线程共享区的方法区(可设置关闭这个区的 GC)
3.2什么时候开始回收?
如今的JVM在java堆里面分了两个代,新生代和老年代。新生代里面有三个区,Eden区和两个survivor区。当Eden区满的时候,会触发一次 Minor GC,会将Eden区中剩余的对象拷贝至一个survivor区中,对象的存活年龄加1,清空Eden区,再次存放对象,等Eden区再次满的时候,触发 Minor GC,将Eden区中剩余的对象和survivor区剩余的对象移动至另一个survivor区中,存活年龄再+1。等年龄增长到了默认值的时候,剩余的对象就会从新生代复制到老年代。当老年代快要满的时候(虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小),如果大于,直接触发 Minor GC. 如果不停的 Minor GC 下去,老年代还是要满了,就会根据配置的参数来决定是要同时执行FULL GC 和 Minor GC 还是只执行 Minor GC 。
3.3 怎么回收?
回收算法有两类:
引用计数收集算法
判断对象的实例是否不再被引用了。当引用为0的时候,就把他标记清除。这样有个问题就是当别的对象实例引用它而引用指针为null的时候,这个就不会被收集。导致垃圾存留。这个算法已经不被使用了。
可达性分析法
可达性分析法都是需要从根GC 扫描判断对象是否被引用,被引用了就标记起来,扫描完了后再扫描一遍内存,查看哪些对象没被标记,没被标记则为待回收对象。针对所有的对象衍生出三种收集方法
- 复制法:新开辟一片内存区域,每标记一个对象,就将标记对象复制一份到新内存上。虽然需要消耗一定的内存,但在对象数量特别少的时候这个算法特别高效(新生代的Eden区使用的就是它)。
- 标记-清除算法: 清除所有待回收对象,然后不对内存进行整理,导致内存空间不连续,生成内存碎片。
- 标记-整理算法: 在清除完所有待回收对象之后,对内存进行整理排序,虽然成本增加了,但效率确实提高了不少。
根GC 对象的选择: 虚拟机栈中所引用的对象,方法区中类的引用的对象,方法区中常量的引用的对象,本地方法区中引用的对象。四种选择方案
3.4 垃圾收集器
- Serial 垃圾收集器
Serial垃圾收集器是最初的垃圾收集器,在GC的时候需要停止用户线程,如果待回收对象太大会造成卡顿现象,适合于client端的java应用。
- ParNew 垃圾收集器
在Serial垃圾收集器的基础上增加了并发收集的实现。可和Cms垃圾收集器一起工作(ParNew负责新生代的收集工作),
Parallel 垃圾收集器
也是并行的多线程收集器,只不过关注点在于控制垃圾收集的吞吐量(其他的关注点在于减少垃圾的收集时间)。Cms垃圾收集器
这个是可以并发执行的一款垃圾收集器,执行流程如下图:
- G1 垃圾收集器
目前为止最牛逼的垃圾收集器,智能型垃圾收集器,虽然原理和流程和Cms垃圾收集器一样,但是里面却有几个不同的概念,比如Region,可预测时间模型等等。
参考: 深入理解JVM(3)——7种垃圾收集器
扩展: JVM内存管理及GC机制
4 内存分配
4.1 对象优先分配到Eden区上
在jvm中,除大对象以外,所有的对象几乎都是优先分配Eden区里面,看以下代码:
配置运行参数为: -Xms20m -Xmx20m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=8 运行结果如下:
这里设置了Eden区大小为8M,两个Survivor区各1M,老年代10M。开始将b1,b2,b3都分配在Eden区,在分配b4的时候,会用到空间分配担保(-XX:+HandlePromotionFailure)技术(jdk8默认开启),然后会将b1,b2,b3 分配至老年代,b4分配到Eden区,占4M
4.2 大对象优先分配到老年代
在jvm的理解中,大对象一般不会变化的,所以会将大对象放入老年代。大对象的确定是动态确定的,根据你设置的Eden去大小而定,大于Eden区,就是大对象,直接将其放入老年代储存。
4.3多次存活的对象分配到老年代
jvm有一个存活阈值,对象生存年龄大于这个阈值就会将对象存入老年代,对象实际进入年老代的年龄是虚拟机在运行时根据内存使用情况动态计算的,有可能存活一两次就进去了。设置阈值的参数是: -XX:MaxTenuringThreshold=15 这个参数指定的是阈值年龄的最大值,默认是15
4.4 逃逸分析和栈上分配
对象放入堆区之前会进行逃逸分析,当方法内的对象不引用别的方法区域内的对象或者全局变量,则称该变量不发生逃逸,会将该变量进行栈上分配,也就是分配到线程独占区的虚拟机栈区里面。否则分配到java堆里面。
本节扩展【JVM】12_空间分配担保
5 性能分析工具
5.1 Jps
jps 就是查看java进程id和开启进程具体命令的一个工具.类似linux的ps命令. 常用使用方法为: jps -mlv (-m: 查看运行的参数,-l: 具体到包名的运行类, -v: 查看运行的vm参数)。 结果截图:
5.2 Jstat
jstat 是用来查看java进程的各种详细信息,类似linux的top命令,它有多个运行参数。常用命令如下: jstat -gcutil [vmid] vmid 即java进程Id(PID),vmid 可以根据jps命令获取到。例如: jstat -gcutil 20412,得到的结果如下所示:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.81 0.00 78.06 32.64 93.06 89.60 784 17.194 70 72.187 89.381
S0: survivor 0 区占用的百分比(KB)
S1: survivor 1 区占用的百分比(KB)
E: Eden 区占用的百分比(KB)
O: 老年代占用的百分比(KB)
M: Metadata 空间利用率(KB)
CCS: 类压缩空间的百分比(KB)
YGC: Minor GC总次数
YGCT:Minor GC总耗时(s)
FGC: Full GC 总次数
FGCT: Full GC总耗时(s)
GCT: 用于垃圾回收的总时间(s)
更多Jstat 选项信息参考jstat命令官方文档
5.3 Jinfo
jinfo命令是用来查看并修改java进程运行的VM参数的命令。使用方法: jinfo -flag [vmid], 例如:
+ 表示启用了这个参数(用的是Parallel GC)
更多使用方法: jinfo -help
5.4 Jmap
jmap 用来查看java进程的堆栈信息,可根据选项打印到控制台或者保存到文件里面.
帮助信息: jmap -help
例如: jmap -dump:format=b,file=f:\jmap.bin 13756
该命令会将 pid为13756进程id的信息保存到f盘的jmap.bin文件里,怎么打开?请看下文
5.5 Jhat
jhat命令就是用来打开jmap命令生成的二进制文件的,应为它是非常耗费内存和cpu的,所以一般会在自己本地环境运行这个命令来查看java进程内存使用信息,而不会在服务端做这种事。打开后,会在http服务器中将这些信息展示出来,默认端口为7000,可通过 -port 参数指定。
帮助信息: jhat -help
例如: jhat f:\jmap.bin
等待一会,看到 Started HTTP server on port 7000 字样的时候,打开 http://127.0.0.1:7000 就可以看到所有的实例信息了,这里我们主要看两个地方,一个是 http://127.0.0.1:7000/histo/ ,查看Heap Histogram,查看的是所有类型对象的实例总数和实例大小。还有一个是 http://127.0.0.1:7000/oql/, 一个oql(Object query Language)查询工具,和sql一样,可以根据条件筛选出指定的对象,例如下面这个语句就是用来查询所有字符串长度大于1000的字符串对象:
更多有关oql的语法可以查看他的帮助文档 http://127.0.0.1:7000/oqlhelp/
5.6 Jstack
jstack 命令就是用来查看java进程的线程信息的一个工具。帮助: jstack -help
例如: jstack -l 13756
会在控制台打印出许多有关的线程信息,其实也就是Thread的getAllStackTraces() 方法返回出来的信息,截取其中的一段显示如下:
|
|
5.7Jconsole 和 VisualVM
Jconsole是jdk自带的可视化分析工具,VisualVM是一个集众多jvm分析插件工具于一身的jvm强大实时可视化分析工具,Jconsole在VisualVM中就是一个插件而已,主页VisualVM。能有效分析内存分配和线程等等指标。VisualVM启动前需要配置jdk的路径,否则会显示找不到jdk。
6 Class文件结构
Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部都是程序运行的必要数据。根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数相当于java中的基本数据类型,以 u1、u2、u4、u8 来分别代表 1、2、4、8 个字节的无符号数。表相当于抽象数据类型,由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表都习惯性地以“_info”结尾。整个 Class 文件本质上就是一张表,它由如下所示的数据项构成:
名称 | 类型 | 数量 |
---|---|---|
magic | U4 | 1 |
minor_version | U2 | 1 |
major_version | U2 | 1 |
constant_pool_count | U2 | 1 |
constant_pool | cp_info | constant_pool_count - 1 |
access_flags | U2 | 1 |
this_class | U2 | 1 |
super_class | U2 | 1 |
interfaces_count | U2 | 1 |
interfaces | U2 | interfaces_count |
fields_count | U2 | 1 |
fields | field_info | fields_count |
methods_count | U2 | 1 |
methods | method_info | methods_count |
attributes_count | U2 | 1 |
attributes | attribute_info | attributes_count |
这个表的顺序就是class文件的解析顺序,接下来看一个 java 类(Hello.java)
编译后的字节码为(Hello.class):
这里的两个数字为一个字节,且都是10进制,首先名字为magic(魔数)的占四个字节,即 CA FE BA BE, 这个是标识这个文件为class文件的,不可能随便改个后缀名就是class文件了吧,读作: 咖啡baby(是不是和java的标志很像). 第二个是 minor_version(次版本), 占两个字节,即 0000, 表明这个class文件的此版本号(class 文件作为一个规范也是有版本号的),接下来是主版本号0034,转成16进制为52,即jdk8对应的版本号。
接下来是constant_pool(常量池)相关的知识点了,常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于 Java 层面的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用总结起来则包括了下面三类常量:
- 类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)
- 字段的名称和描述符(private、static 等描述符)
- 方法的名称和描述符(private、static 等描述符)
虚拟机在加载 Class 文件时才会进行动态连接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
这里说明下符号引用和直接引用的区别与关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
constant_pool 的类型 cp_info 的通用结构如下:
每个constant(常量)开头都是由1个字节的tag标识其属于哪种cp_info,下面是一个字节的info数组(其它信息,这里只是放在一个数组里面进行通用的描述)。在jdk8中,各种各样的constant_pool_tags如下表所示(共14种):
常量类型 | 值(10进制) |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
这里面就包括上面所说的两大类常量:
- 字面量: Utf8,Integer,Float,Long,Double,String
- 引用类型: Class,Fieldref,Methodref,InterfaceMethodref,NameAndType,MethodHandle,MethodType,InvokeDynamic
接下来我们解析一下常量池部分
constatn_pool_count(常量池数量),001F表明有30个常量([1,31)),接下来是constant_pool(常量池), 由cp_info组成,首先根据tag找对应的tag信息(1字节),开始是0A, 二进制的10(#1),找到对应的 CONSTANT_Methodref 类常量(表明这是个引用类型的常量),去官方文档找它的结构如下:
然后找class_index , 两个字节,0004,记住它引用的是4号常量(记作#4),继续,name_and_type_index, 0018, 即24,引用的是24号常量(#24)。
再看下一个常量(#2): 09,表明是一个CONSTANT_Fieldref 类型的引用类型的常量。对应的结构为
和method一样,得到他的引用变量为3号常量(#3)和25号常量(#25).继续这样寻找下去,直到找到 CONSTANT_Utf8 字面量类型的常量,,然后根据他的结构信息
得到它的具体值,length表明的是接下来有几个字节数。下来就是根据这写10进制数找到对应的ascii表的字母了。 其实以上信息可以用jdk自带的javap命令帮你读取出来:
执行: javap -verbose Hello.class
下面这些#号开头的都是我们的常量信息。
常量信息结束后开始解析我们的 access_flags (访问标记),用工具可以看出来我们这里的访问标记为0021,这个是啥意思呢,还是看官方文档给我们的信息
标识名 | 值(16进制) |
---|---|
ACC_PUBLIC | 0x0001 |
ACC_FINAL | 0x0010 |
ACC_SUPER | 0x0020 |
ACC_INTERFACE | 0x0200 |
ACC_ABSTRACT | 0x0400 |
ACC_SYNTHETIC | 0x1000 |
ACC_ANNOTATION | 0x2000 |
ACC_ENUM | 0x4000 |
对几个特殊的标识解释一下:
- ACC_SUPER : 是否有超类(简单解释)
- ACC_SYNTHETIC: 非源码中的类
- ACC_ABSTRACT: 声明为抽象的,不可实例化的类。
这就是我们的类的开始, 这个0021 就是 0020 加上 0001 的结果。也就是说这个类是PUBLIC的而且有父类。
接下来是this_class,占了两个字节,为0003,指向的是3号常量,可以查看上面的常量信息,看到是 Hello 这个类。接下来是 super_class,两个字节,0004,是 java/io/FileInputStream这个类, 下来是interfaces_count,这是因为java是单继承多实现机制的,才会有的这个变量.是0002,表明共有两个接口类,接下来看interfaces,共有两个,分别是0005 和 0006, 代表的是5号和6号变量,Runable 和 ActionListener,
下面继续就是变量,方法,属性的语义部分了,和类差不多,有访问标记,名字和修饰符,不同的是属性就是传参的属性和些格式的问题,可以参考官方文档或者《java虚拟机规范(java se8版)》。其实我们的javap命令还有些输出上面没贴出来,这里加上注释,就能看出class文件后面的大概结构了
扩展:jvm解析系列][六]class里的常量池,访问标志,类的继承关系
7. 类加载机制
先来看类加载流程图
注意: 这里的加载和连接时并行进行的(加载一开始,连接也开始进行),和流程图画的不同。
再来细看每一步jvm做的事情:
7.1 加载
加载需做一下三件事:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
第一条中的二进制流并不只是从class文件中获取,共有以下几种获取方式:
- class文件
- 网络(Applet)
- 计算生成(jdk的动态代理)
- jar包
- 其它文件生成(jsp等)
加载是通过类加载器来进行的,可以自定义类加载器(重写ClassLoader类的loadClass方法)来实现自定义类的加载过程。最终会将加载的二进制流储存进线程共享的方法区中,并且会在java堆中生成一个java.lang.Class对象,这样可以通过这个对象访问方法区中的数据。
7.2 验证
验证主要是验证Class文件中的字节流符合当前虚拟机的规范,主要包括一下四种
- 文件格式验证(魔数)
- 元数据验证(java数据类型)
- 字节码验证(对代码进行校验,保证虚拟机安全运行)
- 引用验证(看常量池中的各种引用符号进行匹配性校验)
7.3 准备
准备是正式的给类变量分配内存和初始值的一个阶段,所有属于类的变量(static)就分配到方法区里面,实例变量则会在对象实例化时随着对象一块分配在java堆中,同时根据不同类型的变量赋予不同的初始值。注意,这个时候并不会直接给变量赋予代码里的那个具体数值(常量除外)。还有几点:
- 对于基本数据类型来说,类变量(static)和全局变量,如果没有给他显示的赋予值,虚拟机会为它赋予默认的初始值.如果是局部变量没有赋予初始值的话,就会报编译错误。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
7.4 解析
前面提到过符号引用这个概念,这个阶段就是将符号引用转为直接引用的过程。解析阶段的顺序不一定是在初始化之前,也有可能是在初始化之后,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
7.5 初始化
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器\
这里简单说明下\
- \
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。 - \
()方法与实例构造器\ ()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的\ ()方法执行之前,父类的\ ()方法已经执行完毕。因此,在虚拟机中第一个被执行的\ ()方法的类肯定是java.lang.Object。 - \
()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成\ ()方法。 - 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成\
()方法。但是与类不同的是:执行接口的\ ()方法不需要先执行父接口的\ ()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的\ ()方法。 - 多线程情况下只会给类执行一遍\
()方法.
7.6 类加载器与双亲委派模型
一张图说明类加载器的执行方式:
- 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器: 重写ClassLoader中的loadClass方法就可自定义实现。主要优点是高度的灵活性,可实现代码的热部署,class文件的解密等等。
下麦呢看一下官方实现的类加载器loadClass方法