首页 > 代码库 > JVM总结
JVM总结
类或者接口可以直接通过类加载器生成。Class文件是一组以8位字节为基础单位的二进制流,按照严格的顺序排列在Class文件里。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号和表
无符号属于基本数据类型,以u1,u2,u4,u8,代表1个字节,2个字节,4个字节,8个字节的无符号数。
表是由多个无符号数或者其他表作为数据项构成复合类型的数据类型,Class文件本质上就是一张表。
魔数和Class文件的版本
每个Class文件的头4个字节称为魔数,它的作用就是确定这个文件是否是一个能被虚拟机接受的Class文件。接着魔数的4个字节存储的是Class文件的版本号。
常量池
版本号之后的是常量池的入口,常量池是Class文件的资源仓库,共有21项常量,主要存放两大类常量:
字面量:如字符串,声明为final的常量值等。
符号引用:类和接口的全限定类名,字段的名称和描述符,方法的名称和描述符
当虚拟机运行的时候需要从常量池中获得对应的符号引用,在类创建时或者运行时解析,翻译到具体的内存地址当中,进行动态连接。
访问标志
在常量池结束之后,接着的两个字节代表访问标志,这个标志用来识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstruct类型,是类的话是否被声明为final,是否是一个注解,枚举等。
类索引,父类索引,接口索引
Class文件由这三项数据来确定这个类的继承关系,类索引用于确定这个类的全限定类名,父类索引用于确定这个类的父类的全限定类名,除了java.lang.Object之外所有的java类都有父类,父类索引都不为0,接口索引集合用于描述这个类实现了哪些接口,如果没有则为0.
字段表集合
字段表用于描述接口或者类中声明的变量,字段可以包括类级变量(static修饰)及实例级变量,但是不能包括在在方法内部声明的局部变量。字段包含的信息有被那个关键字修饰,字段的数据类型。但是这些信息无法固定只能引用常量池的常量描述
方法表集合
和字段表集合类似
属性表集合
Java程序方法体的代码经过javac编译器处理后,变为字节码指令存放在code属性中。但是接口和抽象方法不存在code属性。如果把一个java程序分为代码(code)和元数据(类,字段,方法定义等)两部分那么在class文件中Code用于描述代码,所有其他数据项目都用于描述元数据。
类加载器:
启动类加载器:
这个类负责加载<JAVA_HOME>\bin目录的,或者被-Xbootclasspath参数指定路径的,并且可以被虚拟机执识别的类库,如rt.jar,启动类加载器无法被java程序直接引用,如果需要把加载请求委托给引导类加载器,直接使用null代替。
扩展类加载器:
负责加载<JAVA_HOME>\bin\ext目录的,开发者可以直接使用
应用程序类加载器:
负责加载ClassPath路径下的类库,如果程序没有指定自定以的类加载器一般情况下就使用这个类加载器。
双亲委派模型
类加载器之间的父子关系一般不会以继承关系来实现,而是以组合关系来复用父加载器的代码,双亲委派模型在jdk1.2被引入,但是不是一个强制行性的约束模型。
双亲委派模型的工作过程:
一个类加载器收到了类加载的过程,它首先不会自己去尝试加载这个类,而是把这个类委托给父加载器完成,每一层都是如此,因此所有的加载请求都会被传送到顶层的启动类加载器,只有父类加载器反馈无法完成这个加载请求时,子类加载器才会尝试自己加载。双亲委派模型保证了java程序的稳定性,程序的一致性。
内存管理:
运行时数据区
栈内存
线程私有区域,和线程生命周期相同,描述java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,存储局部变量表,操作数栈,动态链接方法出口等。只有栈顶的栈帧才是有效的,称为当前栈,代表当前的方法。
如果线程请求栈的深度大于虚拟机允许的深度抛出StackOverflowError;
若虚拟机栈支持动态扩展,虚拟机扩展时无法申请到足够的内存,抛出OutOfMemoryError
局部变量表
用于存放方法参数(包括对象的引用)和局部变量。局部变量表所需的内存空间在编译期完成分配,当一个方法进入到栈帧中分配多大的局部变量空间是完全确定的,在运行期间不改变局部变量表的大小。
定义局部变量必须赋初始值
操作数栈
表达式计算在操作数栈中完成
动态链接
常量池的部分符号引用会在类加载阶段或者第一次使用的时候转化为直接引用,称为静态解析。另一部分将在每一次运行期间转化为直接引用称为动态链接。
方法返回地址
堆内存
线程共享区域,此内存区域唯一的目的就是存放对象实例,所有的实例对象和数组都在堆内存上分配。堆无法扩展的时候抛出OutofMemoryError异常
方法区
线程共享区域,存放常量,类变量,类的Class对象
方法区无满足内存分配的需求时抛出OutOfMemoryError.
运行时常量池
用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载之后进入方法区的运行时常量池存放,运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性,运行期间也可以将常量放入其中。
本地放方法栈
为虚拟机使用到的Native方法提供服务
程序计数器
如果线程正在执行的是java方法,记录的是正在执行的虚拟机字节码指令地址。没有内存溢出。
对象创建的过程
当虚拟机遇到一条new指令时,首先会检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载,解析,初始化过。如果没有首先执行相应的类加载过程。
类加载检查完成之后,虚拟机将会为新生的对象分配内存。对象所需要的内存大小在类加载完成后便可以完全确定。
对象在堆中分配内存有两种方式:
如果堆内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放一个指针,分配内存的时候只是将指针往空闲的空间移动与对象相等的一段距离。这种方式称为“指针碰撞 ”。
如果堆内存不是规整的,已使用的内存和空闲的内存相互交错,就必须维护一个列表记录哪些内存可用哪些内存不可用。这种分配方式称为“空闲列表”
选择哪种分配方式由java的堆内存是否规整决定,而java的堆内存是否规整又由所采用的垃圾收集器带有压缩整理的功能决定。
线程安全问题
对象的创建时非常频繁的行为,指针的改变在并发的情况下是非线程安全的。解决这个问题有两种方式:
一种是采用CAS无锁的方式更新保证原子性操作。
另一种是把内存分配按照线程划分在不同的空间之中进行,即每个线程在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程需要分配内存就在哪个线程的TLAB上分配,只有TLAB使用完之后需要新的TLAB才会同步加锁。是否使用TLAB通过-XX:+/-UseTLAB设定。
对象的内存布局
对象在内存的布局可以分为三个部分:对象头(Object Header),实例数据(Instance data),对齐填充(Padding)
对象头包含两部分:第一部分存储对象自身运行时的数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。称为“Mark Word”
另一部分是类型指针,确定这个对象是那个类的实例。
对象访问的定位
两种方式:使用句柄和直接使用指针,在HotSpot使用的是直接使用指针
使用句柄访问:在java堆中开辟出一个句柄池,reference中存储的是对象句柄的地址
使用指针:reference存储的是对象的地址
两种对象的访问方式各有优点,
使用句柄访问最大的好处就是在reference中存储的是稳定的对象句柄的地址,在对象被移动(垃圾收集)的时候只需要改变句柄池中实例数据的指针,reference本身不需要更改。
使用指针最大的好处就是速度更快,节省了一次指针定位的开销。
垃圾收集和内存分配策略
对象是否存活
引用计数算法:给对象中添加一个引用计数器,每当有个地方引用它的时候计数器值加1;引用失效的时候计数器值减1.任何时刻计数器为0时就是对象不再被使用。但是他有一个缺点就是难以解决就是对象间循环引用的问题。
可达性分析算法:GC Roots,向下搜索,走过的路静称为引用链,当一个对象到GC Roots没有任何引用链相连,证明对象不可用。
GC Roots包括:
栈中本地变量表的引用对像
方法区中类变量的引用对象
方法区中常量的引用对象
JNI引用对象
引用类型
JDK1.2之前的引用定义很传统,如果引用类型数据中存储的值代表另一块内存的起始地址,就称为这块内存代表着一个一个引用。
1.2之后:
强引用:默认类型只要强引用还存在,垃圾收集器就不会回收
软引用:SoftReference,内存空间充足不回收,内存空间不足回收
弱引用:WeakReference,一旦出现GC,立即进行回收处理
虚引用: PhantomReference
垃圾收集算法
l 标记—清除算法
不足之处:一是效率问题,标记和清除的效率都不是很高。二是空间问题:标记清除后会产生大量的不连续的内存碎片,空间碎片太多导致以后在程序运行的时候需要分配较大的对象的时候,无法找到足够的连续内存空间,而提前触发另一次垃圾收集动作。
l 复制算法 (新生代使用的算法)
l 标记—整理算法(老年代使用算法)
垃圾收集器
l Serial收集器(新生代使用)
Serial收集器,最基本的垃圾收集器,这个垃圾收集器是单线程收集器,更重要的是它在进行垃圾收集的时候,必须暂停所有的其他工作线程,但是它有着优于其他垃圾收集器的地方:简单高效,对于单个cpu的环境,Serial收集器没有线程上下文的切换,专心于垃圾收集的工作可以获得最高的单线程垃圾收集效率。Serial收集器对于运行在Client模式下的虚拟机来说是不错的选择。
l ParNew收集器(新生代使用)
Serial收集器的多线程版本。但是他却是许多server模式下首选的新生代垃圾收集器。其中一个重要的原因就是新生代中只有他可以和cms收集器组合使用
l Parallel Scavenge收集器
可控吞吐量垃圾收集器。吞吐量=(用户运行代码时间)/(用户运行代码时间+垃圾收集时间)
停顿时间短适合需要和用户交互的程序,高吞吐量可以尽最大的可能完成程序运算的任务,主要适合在后台运算。
Parallel Scavenges收集器提供两个参数用于精确控制吞吐量
最大垃圾收集停顿时间:-XX:MaxGCPauseMillis
直接设置吞吐量大小:-XX:GCTimeRadio
自适应调节策略:-XX:+UseAdaptiveSizePolicy
l Serial Old
老年代Serial 收集器 采用标记-整理算法
l Parallel Old
Jdk1.6版本之后提供
l CMS收集器
一种获得最短停顿时间的收集器,尤其重视服务的响应的速度,希望系统的停顿时间最短,基于标记-清除算法实现。
整个运作过程分为:
初始标记
并发标记
重新标记
并发清除
其中初始标记和重新标记仍然需要“Stop the world”初始标记只是标记一下GC Roots能直接关联的对象,速度很快。重新标记是为了修正因为并发标记期间用户程序继续运作而导致标记产生变动的一部分对象。整个过程之中耗时最长的是并发标记和并发清除阶段但是这两个过程都是可以和用户线程一起并发执行的。
- CMS收集器对CPU资源非常敏感,它虽然不会导致用户线程停顿但是会占用一部分线程而导致应用程序变慢,默认情况下启动的线程是(cpu数量+3)/4,所以当cpu数量不足4个的时候还要分出一半的的运算能力去执行收集器线程,会导致用户应用程序变慢。
- 浮动垃圾,由于在并发清除的阶段用户的线程还在继续运行,伴随着用户程序的运行就会不断有新的垃圾产生,这一部分就称为浮动垃圾。也是由于在垃圾收集的阶段用户线程依旧在运行所以不能等到老年代被填满的时候再进行垃圾回收,要预留一部分空间给并发收集时的用户程序使用,在jdk1.5默认设置下,CMS收集器当老年代使用了68%的空间时就会被激活。如果应用在老年代应用中老年代增长不是很快可是适当提高参数-XX:CMSInitiatingOccupancyFraction提高触发的百分比。
Jdk1.6,CMS启动的阈值提升至92%,但是如果CMS运行期间预留的内存无法满足程序的需要会出现一次“Concurrent Mode Failure”这时虚拟机会启动后备预案:临时启用Serial Old收集器。这样停顿的时间就变长了,所以我们要根据具体情况设置该参数
- CMS是基于标记-清除算法实现的,垃圾收集结束之后会产生大量的空间碎片,为了解决这个问题,使用-XX:+UseCMSCompactAtFullCollection(默认开启),当CMS顶不住要进行Full GC的时候开启内存碎片的合并整理,但是内存碎片的合并整理过程是不能并发的,所以停顿时间会变长,所以另一参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着带来一次压缩的合并整理
l G1收集器
支持并发的垃圾收集,可以独立管理整个GC堆而不需要和其他垃圾收集器配合,从整体上来看是基于标记-整理算法实现,从局部(Region)是基于复制算法实现的,不管怎么样他不会产生空间碎片,还有一个特点就是它可以预测GC的停顿时间。他将整个的堆空间划分为许多个相等的独立空间(Region),虽然依旧保留了新生代和老年代的概念,但是他们不再是物理隔离的了。G1收集器之所以可以预测停顿时间是因为它可以避免在整个java堆中进行全区域的垃圾回收。
初始标记
并发标记
最终标记
筛选回收
GC日志
Full GC说明这次GC是Stop the world
内存的分配和收集策略
- 新生的对象主要是分配在新生代的伊甸园区的,如果启动了本地线程缓冲则按照TLAB进行分配,当Eden区没有足够的空间的时候将会发起一次Minor GC.(Minor GC 是通过复制算法的)
- 空间分配担保机制:在发起MinorGC之前虚拟机会先检查老年代最大连续空间是否大于新生代所有对象占用的总空间,如果这个条件成立,那么可以确保Minor GC 是安全的。如果不成立则虚拟机会查看HandlePromotionFailure是否允许空间分配担保失败,若允许,则继续检查老年代最大连续可用空间是否大于历次新生代晋升老年代对象占用空间的平均值,若大于则进行一次Minor GC。如果不大于则需要进行一次Full GC。
- 大对象直接进入老年代
最典型的大对象就是很长的字符串和数组,最可怕的就是那种朝生夕死的大对象
通过-XX:PretenureSizeThrshold设置令大于这个这个值得对象直接在老年代分配,避免在Eden和两个Survior区之间发生大量的复制。Parallel Scavenge 收集器不认识这个参数。
- 长期存活对象进入老年代
虚拟机给每个对象都定义了一个对象年龄的(Age)的计数器,保存在对象头里,如果Eden中的对象经过一次Minor GC之后任然存活并且能被Survivor容纳的话,将被移动到Survivor中,并且对象年龄设置为1,对象在Survivor中没“熬过”一次Minor GC,年龄增加1岁,当增加到一定程度(默认是15岁)的时候就会被晋升到老年代中。晋升到老年代的阈值可以通过参数
-XX:MaxTenuringThresold设置
5. 动态对象年龄的判断
虚拟机并并不永远要求对象的年龄必须达到MaxTenuringThresold的值才晋升到老年代,如果Survivor中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进如老年代。
JVM总结