运行时数据区
运行时数据区由:程序计数器、虚拟机栈、本地方法栈、堆和方法区组成。
程序计数器:记录当前执行的代码行号,由于java多线程是通过线程轮流切换,分配处理器执行时间来实现,任何一个确定的时刻,一个CPU只能执行一个线程的代码,为例保证下一次线程执行时,能正常继续执行,每个线程需要独立的计数器
虚拟机栈:描述了虚拟机执行方法的内存模型。当执行一个方法时,虚拟机会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、返回地址等。一个方法执行至结束的过程就是一个栈帧从入栈到出栈的过程。
局部变量表存储了编译器便可知的基本数据类型、对象引用和返回地址类型
本地方法栈:与虚拟机栈一样,区别只在于虚拟机栈用于执行java方法,本地方法栈用于执行本地方法,在HotSpot中,其实两者合二为一
堆:堆时虚拟机管理的最大一块内存,为了更好的管理,在jdk1.8及以前又会进一步细分为新生代、老年的,JDK1.7以前还有永久代,JDK9开始,使用region块的形式,开始了内存回收的黄金时代
方法区:用于已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
运行时常量:用于存储编译器产生的字面量和符号引用。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。常量池就是一个类用到的常量的有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用.
例如:
◆类和接口的全限定名;
◆字段的名称和描述符;
◆方法和名称和描述符。
池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用.
二 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性能调优,待以后学了在做更新