java 虚拟机

1 java虚拟机将内存分为不同的数据区域如图:

1.1 程序计数器:是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。java的多线程是通过线程轮流切换并分配处理器执行时间方式实现,任意一时刻,一个内核只会执行一个线程的指令。所以每个线程独立的程序计数器,相互不影响,线程私有内存。线程执行java方法,计数器记录执行的字节码指令地址,如果是native方法,计数器为空。此内存区域是唯一一个java虚拟机规范里没有规定OutOfMemoryError的区域。

1.2.java虚拟机栈:线程私有,与线程生命周期相同。描述的是Java方法执行的内存模型:方法执行时会创建栈帧 保存局部变量表,操作数栈,动态链接,方法出口等。方法从调用到退出,对应入栈出栈。(java内存粗分为栈堆,此栈为彼栈)

局部变量表保存基本数据类型,对象引用和returnAddress类型(字节码指令地址),long和double占用2个局部变量空间(Slot),其余一个,空间大小在编译期间完成分配,运行时不会改变。此区域规定了两种异常:线程请求的栈深度大于虚拟机所允许的深度,将抛出stackOverflowError;虚拟机栈可以动态扩展,如果无法申请到足够的内存,抛出OutOfMenmoryError

1.3 本地方法栈:类似虚拟机栈,虚拟机栈执行java方法,本地方法栈执行native。

1.4 java堆:内存最大的一块,线程共享,虚拟机启动时创建,存放对象实例,是垃圾收集器管理的主要区域,也称“”“GC堆”,GC主要还是分代收集算法,可细分:(新生代 老年代)(Eden,from survivor,to survivor等),线程共享的java堆可能划分出多个线程私有的分配缓冲区。java堆可以在不连续的物理内存空间中。当堆内存中,内存不够时,无法扩展时,抛出OutOfMemoryError

1.5.方法区:线程共享,用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码。也称为“”永久代“,本质上两者不等,仅仅是因为GC分代收集扩展至方法区。不过更容易遇到内存溢出问题。因此设计团队已经采用Native Memory实现方法区规划。同java堆不需要连续的物理空间,同时还可以选择不实现GC。内存回收主要针对常量池回收和类型卸载。无法满足内存分配时,抛出OutOfMemoryError

运行常量池是方法区的一部分,class文件除了类的版本,字段,方法,接口等外还有一项信息是常量池。放编译时生成的字面量和符号引用,在类加载后进入运行时常量池。具有动态性,可运行时,将新的常放入池中,如string.initern()

1.6.直接内存:非java虚拟机,但频繁被使用。NIO是一种基于通道缓冲区的IO方法,它使用native函数库直接分配堆外内存,通过java堆中的DirectByBuffer对象作为引用。提高性能,避免在java堆和native操作。

2 java虚拟机对象

2.1 虚拟机执行new指令时,首先会检查这个指令的参数能否在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否被加载,解析,初始化。如果没有,先执行相应的类加载过程。

类加载后,分配内存,java堆绝对规整分配,分配内存仅仅是把分配指针挪动与对象大小相等的距离(指针碰撞)。不规整,需要建立内存表,记录哪块区域可用(空闲列表),分配方法取决于java堆是否规整,java堆是否规整取决于GC的有无压缩整理。java堆指针移动并不是线程安全的,解决方法:1.分配同步处理,2.java堆分配动作分线程,x线程预先分配一定空间,称为:本地线程分配缓冲TLAB。指令:-XX:+/-UseTLAB。

分配完成后,空间初始化为0(数据类型对应的零值)如果采用TLAB,则在TLAB进行。

对象信息存在对象的对象头中。对象头设置方法根据虚拟机运行状态决定。

虚拟机而言,对象已完成,对于程序而言,还需要<init>,一般情况下new后接着init

2.2 对象在内存中的布局为3个区域:对象头,实例数据,对齐填充

对象头一部分是保存对象自身运行数据:哈希码,GC分代年龄,锁状态,线程持有锁,偏向xianchengID等,称为“Mark Word” 数据长度32/64位一般根据系统。另一部分是类型指针,指向类元数据的指针。

实例数据为代码中定义的各种字段,分配顺序受到虚拟机分配策略和源码顺序,同宽一起,父在子前。如果CompaceFields为true。子类的窄变量在父类变量之间。

对齐填充不是必然存在,占位符,对象起始地址为8字节的整数倍,补位。

2.3 对象引用在栈中,对象在堆中,定位方式取决于虚拟机实现。目前两种:句柄和直接指针

句柄:java堆会划分出一个内存空间为句柄池,reference存储的是对象的句柄地址,而句柄包括对象实例数据(java堆)与类型数据的具体地址(方法区)信息。句柄在GC对象移动中,只会改变句柄。

直接指针:reference存储的直接是对象地址,对象布局考虑类型数据的相关信息。速度快。

3 垃圾收集器与内存分配

3.1对象引用算法:

引用计数器:缺点:循环引用

代码举例:

https://github.com/linrongwu/jvm/blob/master/src/gc/demo/ReferenceCountingGC.java

日志说明:

https://github.com/linrongwu/jvm/blob/master/src/gc/demo/ReferenceCountingGC%E6%97%A5%E5%BF%97.log

可达性分析:GC Roots为根节点,当GCRoots不可达对象时,对象不可用。虚拟机栈,本地方法栈 方法区静态属性 常量

引用分为强引用(只要存在不会回收),软引用(内存不足时,回收),弱引用(活在下次前),虚引用(设计目的是回收时系统通知)。

对象至少要经过两次标记过程:可达性分析不可达,第一次标记并删选:是否执行finalize(),无覆盖或执行过则没必要执行,否则放置到F_Queue中,然后进行第二次标记,如果在finalize()中重新与引用链的对象保持引用关系,它就会被移除

代码举例:

https://github.com/linrongwu/jvm/blob/master/src/gc/demo/FinalizeEscapeGC.java

日志说明:

https://github.com/linrongwu/jvm/blob/master/src/gc/demo/FinalizeEscapeGC%E6%97%A5%E5%BF%97.log

PS:回收方法区

方法区(永久代)主要回收:废弃常量和无用的类。常量与堆的对象类似,类需要满足三个条件:类的实例回收了 加载类的classloader回收,没有反射。可以回收,但不一定回收。

3.2 垃圾回收算法

1 标记-清除:效率不高,且会产生空间碎片问题

2 复制:将空间分区,然后对象的移动,整块清除 举例:1Eden 2Survivor 对象存活率高是效率低

3 标记-整理:一般老年代使用,标记 移动

4 分代收集:目前商业虚拟机都采用,根据对象生命周期内存分块,然后合理使用上面两种

3.3 虚拟机实现:

虚拟机:

OopMap:HotSpot使用一组OopMap数据结构来达到这个目的,类加载完时,HotSpot对对象内偏移量类型数据计算出来,记录下栈和寄存器哪些位置是引用。

安全点:程序只在特定的位置(safepoint)记录这些信息,程序到达安全点是才暂停。对于多线程程序,两种方式:1.抢断:GC先把所有线程中断,如有不在安全点的恢复在到安全点中断, 基本不适用 2.主动:设置标识,每个线程轮询这个标志,当中断标志为真时,中断线程,;轮询标识位置与安全区一致。

安全区域(SafeRegion):当线程处于sleep blocked  不在cpu时间内时,引用关系不发生变化,安全GC 扩展的安全点

3.4 垃圾回收器

Serial:单线程,停止其他线程(默认新生代client模式) 效率高(单线程)

ParNew:Serial多线程版,停止其他线程(默认新生代server模式),与CMS协同

-XX:+UseConcMarkSweepGC或者-XX:+UseParNewGC

Parallel Scavenge:新生代 复制算法 多线程 关注点不一样:吞吐量,而不是线程停顿时间(其他收集器)

最大垃圾收集停顿时间 -XX:MaxGCPauserMillis  吞吐量大小-XX:GCTimeRatio(吞吐量倒数)

-XX:+UseAdaptiveSizePolicy 自适应的调整策略

SerialOld:Serial 老年代版本 Client 如果在Server,则协同Parallel Scavenge或者CMS的后备预案

ParallelOld:Parallel Scavenger 老年代版本 吞吐量

CMS:基于标记清除算法

初始标记:与GCRoots直接关联到的对象

并发标记:GCRootsTracing

重新标记:修正并发标记时期程序运行导致的变动

并发清除:对CPU资源敏感,CM占用的CPU线程数是(cpu数+3)/4

I-CMS:增量式:抢占式线程,不推荐使用

CMS无法处理浮动垃圾,需要预留部分空间,当内存无法满足程序需要,启动后备方法,空间碎片

指令

G1:面向服务端应用 可预测停顿 在全区域收集 java堆划分成许多大小相等的独立区域,跟踪每个Region 优先回收价值最大的Region。Region对应一个RememberedSet 对Reference类型数据进行写操作时,产生一个writeBarrier中断写操作,检查是否处于不同Region 如果是,则通过CardTable 将引用数据记录到RememberedSet中

初始标记

并发标记

最终标记

筛选回收

3.5 对象

新对象一般在Eden,大对象直接老年代, 长期存活老年代,动态对象年龄判断,空间分配担保。

代码日志说明在https://github.com/linrongwu/jvm/tree/master/src/gc/demo

4 Class

Class 是一组以8字节为单位的二进制流。无符号数和表,无符号数以u1,u2,u4,u8表示1,2,4,8字节无符号数,用来描述数字,索引引用,数量值,按照UTF-8编码的字符串。表是已无符号数和其他表的复合结构数据。

class文件前4个字节称为“MagicNumber”,作用是确定文件是否是一个虚拟机接受的class文件。接着后面的4个字节保存的是class文件的版本号,56次版本号,78主版本号,之后是常量池入口,字面量和符号引用。符号引用包括类和接口 字段和方法。再后2个字节就是访问标志:识别类或接口的访问信息。类索引,父类索引都是一个u2类型的数据,接口索引是一组u2类型的数据:入口第一项是索引大小。字段表集合用于描述接口或者类中的声明的变量。方法表类似字段表,属性表用于描述某些场景信息。

5 类加载

类的生命周期:加载 连接(验证,准备,解析) 初始化 使用 卸载,其中加载 验证 准备 初始化 卸载 是确定的(执行顺序),初始化阶段,虚拟机规定了以下情况必须初始化(加载在初始化前)

5.1 遇到new(创建) getstatc(读取) putstatic(放置) invokestatic(执行)

5.2 反射调用

5.3 子类初始化前 父类初始化

5.4 main()所在的类会初始化

5.5 动态语言特性

对于静态变量 只有直接定义的类会被初始化

对于数组 不会触发类的初始化

常量在编译阶段存入类的常量池中,本质上没有直接引用常量的类,没有触发初始化

加载完成以下事情

1 通过全限定类名获取二进制字节流

2 将字节流代表的静态数据结构转化为方法区的运行时数据结构

3 在内存生成一个代表这个类的Class对象,作为方法区这个类的各种数据访问入口

验证:验证安全

准备:正式分配内存

解析:解析是将常量池的符号引用替换直接引用过程

任何一个类的唯一性由类加载器和他本身决定。

双亲委派:两类:启动类加载器(C++,虚拟机的一部分),继承抽象类Classloader。

继承类划分:

扩展类加载器<Java_home>\lib\ext,开发人员可以使用

应用程序类加载器用户类路径上的类,开发人员可以使用

双亲委派模式以组合形式实现不是继承

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值