运行时数据区
运行时数据区由:程序计数器、虚拟机栈、本地方法栈、堆和方法区组成。
程序计数器:记录当前执行的代码行号,由于java多线程是通过线程轮流切换,分配处理器执行时间来实现,任何一个确定的时刻,一个CPU只能执行一个线程的代码,为保证下一次线程执行时,能正常继续执行,每个线程需要独立的计数器.
特点:线程独有、没有GC、不会OOM。虚拟机栈:描述了虚拟机执行方法的内存模型。当执行一个方法时,虚拟机会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、返回地址等。一个方法执行至结束的过程就是一个栈帧从入栈到出栈的过程。设置大小:-Xss
栈帧的结构
a. 局部变量表存储了编译期便可知的基本数据类型、对象引用和返回地址类型。
b. 操作数栈暂存字节码指令的值用于运算
c. 动态链接指向运行时常量池中该栈帧所属方法的引用。(java源文件编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里----为了节约空间。动态链接的作用就是将符号引用转换为直接引用。)
d. 方法返回地址
e. 附加信息
背景:由于跨平台性的设计,java指令都是根据栈来设计。不同平台cpu架构不同,所以不能采用基于寄存器实现。
栈的特点:跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能,相比寄存器需要更多的指令。
虚拟机栈特点:机栈的生命周期与线程相同。不存在GC,会OOM本地方法栈:与虚拟机栈一样,区别只在于虚拟机栈用于执行java方法,本地方法栈用于执行本地方法,在HotSpot中,其实两者合二为一
堆:堆是虚拟机管理的最大一块内存,为了更好的管理,在jdk1.8及以前又会进一步细分为新生代、老年的,JDK1.7以前还有永久代,JDK9开始,使用region块的形式,开始了内存回收的黄金时代。设置大小 -Xms -Xmx
TLAB:在堆上为每个线程分配单独的空间
逃逸分析:对于经过逃逸分析,发现变量没有脱离本方法调用域的,可以考虑栈上分配
标量替换:如果一个对象是有简单的标量组成,可以考虑使用标量来代替这个对象。进而实现栈上分配等优化方法区:用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
运行时常量:用于存储编译器产生的字面量和符号引用。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。常量池就是一个类用到的常量的有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用.
例如:
◆类和接口的全限定名;
◆字段的名称和描述符;
◆方法和名称和描述符。
池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用.
1)方法区的演进
1.6 有永久代,静态变量存放在永久代中;
1.7 开始“去永久化”,将字符串常量池、静态变量放到堆中
1.8 无永久代,类型信息、字段、方法、常量保存到本地内存的元空间,将字符串常量池、静态变量放到堆中 。
2)永久代为何要被元空间替换
a. 为永久代设置空间大小是很难确定的
b. 对永久代的调优是很困难的
3)常量池为何要从方法区放到堆中
因为永久代的回收率很低,在full gc的时候才会触发。而实际中有大量字符串会被创建,并且可以被回收。
补充:
在JVM中,将符号引用转换为直接引用与方法的绑定机制相关
- 静态链接:编译期便可知的
- 动态链接:编译期不能确定下来的
方法绑定机制
- 动态绑定(晚期绑定)
- 静态绑定(早起绑定)
虚方法与非虚方法
非虚方法:在编译期就能确定具体的调用版本,这个版本在运行时是不可变。如静态方法,私有方法、final方法、实例构造器、父类方法。
二 HotSpot虚拟机
对象的创建(简单理解,具体的细节由后面的类加载一章再做详谈)
检查:当虚拟机遇到关键字new时,首先会检查new对象所对应类,在常量池中是否存在,并且已被加载、解析和初始化;如果没有,则需要先进行加载。
分配内存:当类加载完成后,所需要的内存空间大小就已经确定的,接下来就需要为其分配空间,而分配方式由内存是否规整分为两种
指针碰撞:如果内存空间是规整的,使用的内存与没用使用的内存使用指针隔开,则分配内存时,只需要将指针往空闲位置移动与类空间相等大小的位置。
空闲列表:如果内存空间不规整,则虚拟机需要维护一张表,用于记录哪些内存是可用的,当分配内存时,需要查表寻找一块大小合适的内存进行分配,然后更新表信息。
除了考虑内存分配外,还需关心对象的线程安全性问题
由于对象的创建在虚拟机中相对频繁,仅仅一个指针的改动,在并发情况下也是不安全的,解决方式:
1)采用CAS+失败重试机制保证更新的原子性;
2)使用本地线程分配缓冲,即提前为每个线程预先分配一小块内存。
内存初始化:将分配的内存初始化为系统默认值,如果这里采用了本地线程缓冲,则该初始化可以提前到分配线程缓冲时。接着还要进行必要的一些设置,为后面的使用做好准备,比如对象头中存放属于哪个类的实例、GC分代年龄、哈希码等设置初始零值,最后执行构造<clinit>,此时一个真正可用的对象才算完成
对象的内存布局
- 对象头:对象头分为两部分
第一部分用于存储自身运行时的数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等;
第二部分存储类型指针,执行它的类型的元数据指针,通过该指针,可以知道是哪个类的实例,但这不是唯一的方法- 实例对象:存放真正有效的数据,即我们代码中定义的字段以及从父类继承下来的
- 对齐填充:可有可无
对象的访问方式
- 直接引用:效率高
- 句柄:对象被移动时,改变的只是句柄中的实例数据指针,而reference本身不变
虚拟机是如何执行字节码的
从虚拟机以及底层硬件两个角度。
虚拟机视角来看
- 执行java代码首先需要将它编译成class文件加载到虚拟机中,加载后的Java类会存放于方法区,实际运行时,虚拟机会执行方法区的代码。
- Java 虚拟机会将栈细分为虚拟机栈(在hotspot中,虚拟机栈和本地方法栈是没特别区分的),以及存放各个线程执行位置的 程序寄存器
- 在运行过程中,每当调用进入一个 Java 方法,虚拟机会在当前线程的 Java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。
从硬件视角来看
- java字节码无法直接执行。因此虚拟机需要将字节码翻译成机器码;
- 翻译的过程有两种形式:
第一种是解释执行,即逐条将字节码翻译成机器码并执行;
第二种是即时编译,即将一个方法中包含的所有字节码编译成机器码后再执行。
解释执行优势是无需等待,即时编译优势实际运行速度更快。HotSpot默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译
深堆和浅堆
浅堆表示一个对象结构所占用的大小(对象头+实例数据+对齐填充,不包括内部引用 对象大小)
深堆表示一个对象被 GC 回收后,可以真实释放的内存大小(保留空间)
如何查看对象在堆中的大小
jmap -histo PID 查看对内对象占用空间大小,有高到低排序
关于jvm性能调优,待以后学了在做更新
本文详细介绍了JVM运行时数据区,包括程序计数器、虚拟机栈、本地方法栈、堆和方法区等的组成和特点,还阐述了HotSpot虚拟机中对象的创建、内存布局、访问方式,以及虚拟机执行字节码的方式,同时介绍了深堆和浅堆概念及查看对象堆大小的方法。


被折叠的 条评论
为什么被折叠?



