1.JVM基础
1.1 java历史
oak,詹姆斯.高斯林,write once run anywhere.
1995.5 Oak Java1.0 Write once run anywhere
1996.1 jdk1.0 jvm Sun Classic VM
1996.9 首届 JavaOne大会
1997.2 jdk1.1 内部类,反射 jar文件格式,jdbc,Javabeans,rmi
1998 1.2 J2SE j2EE j2ME swing jit Hotspot VM
2000.5 jdk1.3 Timer java2d
2002 2 jdk1.4 Struts , Hibernate , Spring 1.X
正则表达式
Nio
日志
Xml解析器
2004.9 jdk1.5 tiger 自动装箱拆箱,泛型,注解,枚举,变长参数,增强for循环 Spring 2.x Spring4.x
2006 jdk1.6 java EE java Se javaMe jdk6
提供脚本语言支持
提供了编译api以及http服务器api
2009 jdk1.7 收购sun 74亿
2011 7 jdk1.7
2014.3 jdk1.8
复制代码
goto是保留关键字但不用.
1.2 JDK8新特性
- 接口的默认方法和静态方法
Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法。默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
- Lambda 表达式和函数式编程
- Date API
- 重复注解
- 更好的类型推断
Java 8编译器在类型推断方面有很大的提升,在很多场景下编译器可以推导出某个参数的数据类型,从而使得代码更为简洁
- Nashorn JavaScript引擎
- JVM修改
-
- 使用Metaspace(JEP 122)代替持久代(PermGen space)。
-
- 在JVM参数方面,使用**-XX:MetaSpaceSize和-XX:MaxMetaspaceSize**代替原来的-XX:PermSize和-XX:MaxPermSize。
1.3 java运行机制
1.4 jvm产品
- Sun Classic VM
jdk1.1中包含,当前已经落后,世界上第一款商用的Java虚拟机,只能使用纯解释器的方式来执行Java代码
解释器和编译器不能同时工作.
- Exact VM
Exact Memory Management 准确试内存管理,jdk1.2时发布
编译器和解释器混合工作以及两级即时编译器
只在Solaris平台发布,没有在linux和windows发布(linux和windos此时为Sun Classic VM)
英雄气短
- HotSpot VM
当前运行最广的.最开始不是sun开发,98年sun收购.
热点代码技术.通过计数器,2006开源
- KVM (Kilobyte)
Kilobyte 简单,轻量,高度可移植,
在手机平台运行
- JRockit
BEA,2008,世界上最快的Java虚拟机专注服务器端应用,
优势:垃圾收集器
MissionControl服务套件
BEA JRockit Mission Control,用来诊断泄漏并指出根本原因。该工具的开销非常小,因此可以使用它来寻找生产环境中的系统的内存泄漏。
BEA JRockit Mission Control(以下简称为JRMC)于2005年12月面世,并从JRockit R26.0.0版本开始捆绑了这个工具套件,目前最新的版本是2.0.1。它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。它包括三个独立的应用程序:内存泄漏监测器(Memory Leak Detector)、JVM运行时分析器(Runtime Analyzer)和管理控制台(Management Console)。
- J9
J9(内部代码) 全称:IBM Technology for Java virtual Machine IT4j
- Azul VM
高性能的Java虚拟机
- Liquid VM
高性能的Java虚拟机
- Dalvik VM
Google,Dex dalvik Executalbe
- Microsoft JVM
- Taobao VM
基于hotspot深度定制的
2.Java内存区域
2.1 Java内存区域-简介
2.1.2 概述
- Heap(堆内存):
使用Java语言创建的所有的引用对象类型,都在此存储。并由 GC (Garbage Collection)对其进行管理,
诸如:释放不再被程序引用的对象所占据的内存。
- Stack(栈内存):
与 Heap 相对的是,Stack 存放基础数据类型。诸如:int, char 等。
由程序的执行顺序控制变量的进出栈顺序,而不是由 GC 控制栈内存的管理。
- Perm(持久内存):
用于存储类的元数据。诸如:类的定义,方法的定义等。
Perm 的生命周期与 JVM 绑定,而 Heap 的生命周期与程序绑定。
2.2 Java内存区域-Java虚拟机栈(VM Stack)
2.2.1 简介
- 虚拟机栈 : 描述的是Java方法执行的动态内存模型,虚拟机栈存放栈帧的进出.
- 线程独占 :伴随方法,所以独占.
- 栈帧 : 一个方法会产生一个产生一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接,方法出口等。如果方法调用其它方法,其它方法也调用栈帧, 然后调用的方法进栈,然后调用完毕之后就出栈然后销毁,然后最后原方法执行完毕然后最后出栈.
- 局部变量表
存放编译期可知的各种基本数据类型,引用类型,returnAddress类型。
局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小.
引用类型:user对象,name属性(String ),局部变量寸的只是对象的引用,引用的大小是不会变的.
- 栈的大小
-
- 要放很多的栈帧,所以可能会放不下,然后就会StackOverflowError(栈内存溢出,比如递归调用的时候会发生)
-
- 如果栈的区域大到超出内存就会 OutOfMemory(申请不到内存了)
2.2.2 栈帧(Stack Frame)
-
栈帧(Stack Frame) 是虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素。
-
每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成。每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程,栈帧出栈就销毁。
-
每一个栈帧在编译程序代码的时候所需要多大的局部变量表,多深的操作数栈都已经决定了,并且写入到 Code 属性之中,一次一个栈帧需要多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。
-
一个线程中方法调用可能很长,很多方法都处于执行状态。对于执行引擎来说,只有处于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与之相关联的方法称为当前方法(Current Method) 。
-
栈帧组成典型的栈帧主要由 局部变量表(Local Stack Frame)、操作数栈(Operand Stack)、动态链接(Dynamic
Linking)、返回地址(Return Address)(方法出口)
2.2.2.1 局部变量表
-
作用: 存放编译期可知的各种基本数据类型,引用类型,returnAddress类型。
-
局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小.
-
局部标量表 是一组变量值的存储空间,用于存放 方法参数 和 局部变量。在Class 文件的方法表的 Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量。
-
- 变量槽 (Variable Slot)是局部变量表的最小单位,没有强制规定大小为
32位,虽然32位足够存放大部分类型的数据。一个 Slot 可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8种类型。其中 reference 表示对一个对象实例的引用,通过它可以得到对象在Java 堆中存放的起始地址的索引和该数据所属数据类型在方法区的类型信息。returnAddress 则指向了一条字节码指令的地址。 对于64位的 long 和 double 变量而言,虚拟机会为其分配两个连续的 Slot 空间。
-
虚拟机通过索引定位的方式使用局部变量表。局部变量表存放的是方法参数和局部变量。当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用,即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。
-
为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。当离开了某些变量的作用域之后,这些变量对应的 Slot 就可以交给其他变量使用。这种机制有时候会影响垃圾回收行为。
public static void main(String[] args){
{
byte[] placeholder = new byte[64*1024*1024];
}
System.gc();
}
运行结果:
[GC 602K->378K(15872K), 0.0603803 secs]
[Full GC 378K->378K(15872K), 0.0323107 secs]
[Full GC 66093K->65914K(81476K), 0.0074124 secs]
public static void main(String[] args){
{
byte[] placeholder = new byte[64*1024*1024];
}
int a = 0;
System.gc();
}
运行结果:
[GC 602K->378K(15872K), 0.0018270 secs]
[Full GC 378K->378K(15872K), 0.0057871 secs]
[Full GC 66093K->378K(81476K), 0.0054067 secs]
复制代码
2.2.2.2 操作数栈
-
操作数栈(Operand Stack)也常称为操作栈,是一个后入先出栈。在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java 虚拟机的解释执行引擎称为”基于栈的执行引擎“,这里的栈就是指操作数栈。
-
方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的。
-
在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传
2.2.2.3 动态连接
-
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
-
Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接
2.2.2.4 方法返回地址
-
当一个方法开始执行以后,只有两种方法可以退出当前方法:
-
- 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
-
- 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
-
当方法返回时,可能进行3个操作:
-
- 恢复上层方法的局部变量表和操作数栈
-
- 把返回值压入调用者调用者栈帧的操作数栈
-
- 调整 PC 计数器的值以指向方法调用指令后面的一条指令
2.2.2.5 附加信息
- 虚拟机规范并没有规定具体虚拟机实现包含什么附加信息,这部分的内容完全取决于具体实现。在实际开发中,一般会把动态连接,方法返回地址和附加信息全部归为一类,称为栈帧信息
2.2.3 大小
如果该栈满了则报:StackOverflowError
如果栈很大,并且能自动扩大,就可能报OutOfMemory
2.2.4 补充
- 栈内存由使用的人向系统申请,申请人进行管理。
2.3 Java内存区域-本地方法栈
2.3.1 概述
- 线程独享
- hotspot虚拟机中本地方法栈和虚拟机栈在一起,合二为一了
- 虚拟机栈为虚拟机执行Java方法服务.
本地方法栈为虚拟机执行native方法服务.
2.3.2 Java虚拟机栈和本地方法栈对比
2.4 Java内存区域-程序计数器
程序计数器(program counter register):
- 是当前线程所执行的字节码文件(class)的行号指示器.
在虚拟机的中,字节码解释器就是通过改变计数器的值来选取下一条执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要程序计数器来实现的。
保存正在执行的字节码(虚拟机栈)指令的地址,如果执行native方法(JVM中为本地方法栈),这个计数器的值为 undefined。
- 线程独占
因为处理器在一个确定的时刻只会执行一个线程中的指令,线程切换后,是通过计数器来记录执行痕迹的,因而可以看出,程序计数器是每个线程私有的
- 占用空间小,可以忽略不计,不会引起OutOfMemoryError(JVM中)
此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.因为开发者不操作这块,由虚拟机内部维护.
2.5 Java内存区域-堆存(Heap)
2.5.1 堆内存概述
- 存放对象实例,垃圾收集器管理的主要区域,回收效率最高的地方
- 简单的来讲,堆内存用于存放由new创建的对象和数组,在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。
2.5.2 堆内存分类
- 新生代
-
- Eden 伊甸园
-
- Survivor 存活区 (Survivor0,Survivor1)[/sɚ’vaɪvɚ/ ]
- 老年代 Tenured Space
分类的目的是为了垃圾回收.
2.5.2.1 新生代
-
所有新创建的 Object 首先被放在 Young Generation 内存区。 如果 Young Generation 内存区满了,则执行 Garbage Collection 。这种 GC 称为 Minor GC。
-
Young Generation 区又分为三部分: Eden Memory,Survivor0 Memory (S0),Survivor1 Memory(S1).
-
绝大多数新建的 Object 被放在 Eden Memory ;
-
如果 Eden Memory 内存满了,则进行 GC 操作。同时把未被 GC 的 Object 移动到 S0 或 S1 中。
此时 Minor GC 也会检查和移动 S0 和 S1 中的对象。 最后使 S0,S1 其中一个置为空。 -
多次 GC 后仍然未被 GC 的 Object 将被移动到 Old Gen 内存区中。 通常 Object 会被 GC 设定一个轮询的阀值。
2.5.2.2 老年代
-
Old Gen 内存区存放了经过多次 Minor GC 后仍然不能被 GC 的 Object。
-
与 Young Gen 相同,当 Old Gen 区满了之后将执行 GC 操作,该操作称为:Major GC。 耗用的时间也相对较长
-
stop-the-world 事件
Young Gen 和 Old Gen 都可以主动触发 stop-the-world 事件,挂起所有任务,执行 GC 操作。
被挂起的任务只有在 GC 执行完毕后,才会恢复执行。
- 多数情况下, GC 性能调优(GC tuning)就是指降低 stop-the-world 时 GC 执行的时间。
2.5.3 堆内存异常及配置
-
OutOfMemory
-
-Xms
设置JVM启动时的堆内存(Heap)的大小
-
-Xmx 设置堆内存(Heap)的最大值 ,For setting the maximum heap size.
-
-Xmn 设置 Young Gen 内存区的大小
-
-XX:PermGen 设置 Perm Gen 内存的初始大小
-
-XX:MaxPermGen 设置 Perm Gen 内存的最大值
-
-XX:SurvivorRatio 设置 Eden Gen 与 S0 Gen,S1 Gen 内存的大小比。默认值:8
例如: Young Gen 大小为 10M, -XX:SurvivorRatio=2 ;则: Eden Gen 的大小为 5, S0 和 S1 的大小分别为2.5
- -XX:NewRatio 设置 Old Gen / Young Gen 的值。默认:2
2.6 Java内存区域-方法区
2.6.1 方法区(HotSpot永久代)概述
- 在HotSpot虚拟机选择把GC分代收集扩展至方法区,或者说是用永久代来实现方法区.
这样做的目的:HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作.
-
在虚拟机规范及其它虚拟机不是这样.
-
永久代 Perm Gen(Permanent Generation,JDK1.8取消) ===> 元空间(Metaspace,JDK1.8)
2.6.2 方法区功能
- Perm Gen 区是一个特殊的JVM内存区,因为它用来存储用来描述 Class 的元数据(application metadata;Class可以不属于Java语言的一部分,也可以属于)
诸如:类信息,常量,静态变量,即时编译器编译后的代码等数据、类的版本、字段、方法、接口、以及常量池。
- Perm Gen 被 JVM 使用于应用程序运行期间(runtime),基于应用所使用到的类。
- Perm Gen 中同时包括 Java SE 包中的类。
2.6.2.1 运行时常量池(Runtime Constant Pool)
常量池存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量中.
== 比较的是引用地址
String a ="abc"; //a是引用存在栈的局部变量表,"abc"不是在在java堆中,而是在方法区的常量池(StringTable:HashSet)中. 也是字节码常量
String b ="abc"; //b是引用存在栈的局部变量表,"abc"不是在在java堆中,而是在方法区的常量池中.也是字节码常量
String c = new String ("abc");//new 创建的对象都存放在堆内存中.
a = b true
a = c false
a = c.intern true //c.intern 运行时常量.将堆中的值拿到 运行时常量中.
复制代码
2.6.3 垃圾回收
- 垃圾回收在方法区的行为:垃圾回收少,因为少回收,效率低.
- Perm Gen 只有在执行 Full GC 时才会被 GC。
2.6.4 方法区异常及配置
- java.lang.OutOfMemoryError: PermGen 然而无论你怎么设置 -Xmx 也不管用。
因为设置其大小的参数不是 -Xmx,而是
- -XX:PermGen, -XX:MaxPermGen (不同Java版本略有变化)
2.6.5 补充
-
Perm Gen 不是 Heap 的一部分
-
以后会发现不是所有的对象实例都放在堆上. 但是可以理解为对象实例都在堆上.
还会在堆内存中划分线程私有的内存去. 目的也是为了垃圾回收.
- 字符串在常量池中,一般对象都在堆中,
2.7 方法区JDK8(JDK8 元空间(Metaspace)取代永久代(Perm Gen))
JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)
2.7.1 替换原因
- 移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
- 由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen
2.7.2 元空间功能
-
元空间是方法区的在HotSpot jvm 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
-
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
2.7.3 常用配置参数
- 1.MetaspaceSize
初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数
- 2.MaxMetaspaceSize
限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
- 3.MinMetaspaceFreeRatio
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
- 4.MaxMetasaceFreeRatio
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。
- 5.MaxMetaspaceExpansion
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
- 6.MinMetaspaceExpansion
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。
2.8 直接内存(Direct Memory)
- 直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域.
- 这块内存频繁使用,也可能导致OutOfMemoryError异常
2.9 对象在内存中的布局
2.9.1 对象的创建
- 对象创建步骤:
1)根据new的参数在常量池中定义一个类的符号引用
2)如果没有找到这个符号引用,说明类还没有后被加载,则进行类的加载,解析和初始化.
3)虚拟机为对象分配内存(位于堆中)
4)将分配的内存初始化为零值(不包含对象头)
5)调用对象的方法
- 对象分配内存的策略:
1)指针碰撞(规则内存块)
2)空闲列表:不规则的内存,有一个记录空闲内存的表,然后去找空闲的内存区域.分配.
空闲内存是否规则,是根据垃圾策略来判断的.如果有压缩整理的功能,则为规则用指针碰撞.如果没有压缩整理功能则不规则用空闲列表.
- 线程安全
一个空间的内存可能被同时2个申请:
方案1)线程同步,加锁,安全,但是慢执行效率低.
方案2)针对每个线程单独分配一块区域,本地线程分配缓冲(TLAN),每个线程需要分配的时候,就从本地电池分配缓冲中分配,就不会和其它的线程的区域冲突,如果满了就再分一块(分配新的区域的时候使用同步锁策略).
- 初始化对象
初始化默认值,整数型默认0,boolean类型默认false,对象默认null.
- 执行构造方法
初始化完成后会执行构造方法.
2.9.2 对象的结构
- Header(对象头)p47
存储的信息有:
-
- 自身运行时数据(Mark Word)长度大小与32位虚拟机和64位虚拟机不一样,32位32字节,64位64字节
哈希值 Ojbect的hashCode是调用的natvie方法.
GC分代年龄,对内存中分代:标记清楚,标记整理,复制,分代收集
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
- 自身运行时数据(Mark Word)长度大小与32位虚拟机和64位虚拟机不一样,32位32字节,64位64字节
以上图中发现对象在锁状态不一样的时候用同样的空间表达不同的内容,让空间进行复用.
-
- 类型指针
不是所有的都需要类型指针.
-
- 数组长度
如果是数组,在对象头中还有一部分来保存数组长度.
- InstanceData
存储的对象的有效信息, 就是我们看到的对象.
相同分配的字段,会在一块 Longs/doubles;shorts/chars .
- Padding
填充,也不是必然存在,就是占位符,为了填充没有实际意义.原因:自动内存管理要求对象起始地址必须八个字节的整数倍,对象头也需要是八个字节的整数倍P48.
2.9.3 对象的访问定位
p48
规定了栈内存指向堆内存的引用地址.但地址不一定是对象本身.也可以是句柄.
-
使用句柄
有一个句柄池,保存实例对象地址,然后找到对象的真正地址.好处是句柄池的地址可以不用改变(此时不管对象移动位置还是垃圾回收了).改变的句柄池中的引用地址.栈的指向不用改变,性能高. -
使用直接指针
不过HotSpot使用这种方式. -
总结:对象构造
-
- 使用句柄时:
栈,堆(句柄[保存到对象实例数据的指针{指向堆中对象},到对象类型数据的指针{指向方法区}共2个指针],对象),方法区(对象类型数据区域)
-
- 直接指针时:
栈,直接对象(保存到对象类型数据的指针{指向方法区}),方法区(对象类型数据区域)
2.10 补充
- 常量池: 是Class结构文件中的常量池,运行时到运行时常量池中.
2.11 java代码内存空间-实战及参考文章
深入理解java虚拟机书