JVM、Runtime、字节码与垃圾回收
Java Vitrual Machine
Class
实际上是二进制内容,包含了java中的所有信息
类文件文件格式
- 文件头magic u4(4个字节) 表明了不同文件格式
- 次版本号 u2
- 主版本号 u2
- 常量池 元素数量 u2
- 常量池 *
- 访问标志 u2 (super public等)
- ……
无分隔符式的文件结构,可以确定长度的用指定长度字节,比如版本号固定两个字节,不能确定的先有一个八位字节(一个int值)表示后面数据的长度,然后再写具体数据,比如名字
反编译字节码
javap
javap -c
Javap -v
方法的字节码
stack操作数栈,表明操作需要几个栈,比如1+1就要两个存放两个1,当+的时候两个1出栈一个2入栈
lacals本地变量表大小,存放计算过程中的临时结果
args_size传入 参数表,非静态变量会有一个默认的this
指令,每一个指令都占一个字节,前面有一个数字,记录了指令的程序偏移量,这个会被程序计数器记录,走if之类的会用到
JAVA的一些应用
a = i++与a = ++i
i++是先把本地变量表中的i拿到操作栈中,然后对本地变量表中的i+1,然后把操作栈中的数出栈赋给本地变量表中的a
++i是先对本地变量表中的i+1,然后拿到操作栈,然后把操作栈中的值给a
内部类持有外部类引用
public class Hello {
void main() {
new Inner();
}
class Inner {
}
}
在字节码中 Inner会持有一个Hello的成员并且够造方法里也有Inner参数,然后再构造方法里会把Inner赋值
kotlin对此做了优化,当内部没有使用到this的时候,不会持有外部类引用
泛型擦除
List
sychronized
monitorenter
monitorexit 这个有两次,因为怕方法内异常终止,所以再次调用确认结束锁
JVM运行时数据区 Runtime Data Area
heap堆
存放对象 gc发生的地方 内存最大的地方
method area方法区
记录被jvm加载进来的class数据,包含字段、方法、构造器、以及各种操作
常量池也在这里
Java stacks java栈
存放栈帧的地方,执行方法时会进行入栈出栈操作
线程创建时创建,线程里同时还有程序计数器(记录方法执行到哪一行,操作符前的用的是偏移量)
native method stacks 本地方法栈
线程创建时创建
程序计数器 pc registers
记录代码(字节码中)的执行位置,使用的时偏移量,也就是正在执行的字节码指令的地址,如果是native方法,这里不会有值
执行一个方法时运行时数据区的具体情况
public class Hellow {
public static void main(String[] args) {
Hellow hellow = new Hellow();
hellow.greeting()
}
public void greeting() {
System.out.printlin("Hellow")
}
}
#字节码
public static void main()
descriptor: ···
flags: ···
Code:
stack=2, locals=2, args_size=1
0:new
3:dup
4:invokespecial
7:astore_1
8:aload_1
9:invokevirtual
12:return
- 虚拟机启动时,heap和method area就已经创建
- 线程创建后, java栈 native栈和程序计数器创建
- 执行main方法时,先把通过classloader类加载器把类加载到method area
- 调用方法,创建栈帧压入虚拟机的Java stacks,一个栈帧由三部分组成:locals 本地变量表,stacks 操作数栈,frame data 负责动态链接以及方法出现异常和方法返回操作,动态链接就是根据字节码中的引用去寻址
- 执行new,去堆上创建对象并把引用给到操作数栈,此时仅仅是创建了对象,并未初始化
- 执行dup,复制操作数栈里的值并压入操作数栈,此时操作数栈中有两个引用,并且程序计数器标记为3
- 执行invokespecial,执行对象初始化操作,调用对象的构造方法,这时操作数栈会出栈一个引用,创建一个新的栈帧入栈,把之前出栈的引用给新的栈帧,这个栈帧在完成初始化后出栈,此时对象初始化完成
- 执行astore_1,把操作数栈的对象赋值给本地变量表1号位,这里就完成了
Hellow hellow = new Hellow();
- 执行aload_1,把本地变量一号位加载到操作数栈
- 执行invokevirtual,传入对象引用,调用另一个方法
- 另一个方法入栈、执行、出栈,然后此方法也return出栈
Java对象在堆中的结构
组成:
对象头
如果对象非数组:
- markword,会根据对象锁的状态不同而变化 在32位系统里有32位
- 最后三位0 01:无锁,前25位是hashcode,中间4位是age(gc年龄)
- 最后三位1 01:偏向锁,此时指只有一个线程会访问锁,这时会记录线程的id方便访问,前23记录线程id,2位记录偏向锁时间戳,4位记录age,如果记录的id与正在访问的线程id一致则直接获取锁,如果不是则尝试替换threadid,如果失败就升级位轻量级锁
- 后两位是10:重量级锁,30位记录锁的指针(pointer to heavyweight monitor ),hashcode在指针内部,多线程进入后竞争锁,失败进入阻塞状态
- 后两位00:轻量级锁,30位记录指针,竞争失败后会继续尝试获取锁,也叫自旋锁,自选次数有限,达到此书后锁会升级位重量级锁
- 后两位11:标记为gc
值得注意:无锁状态下hashcode被调用后才会生成,偏向锁是没有hashcode的,在偏向锁状态下调用hashcode会直接升级为重量级锁
- 类型指针,对象的引用出自这里
是数组
多一个数组长度区域
具体数据
代码中的具体数据,比如成员变量等
对齐填充
确保对象大小为8的倍数
JVM垃圾回收
发生在heap中
判断对象是否能被jcroot直接或间接引用到
标记清除法 mark and sweep
gc时root对可以访问的对象进行标记,全部标记后会对没有标记的对象进行回收
但会产生内存碎片
标记整理法 mark compact
把标记后的对象直接向一端移动,然后再回收
但效率不高
复制 Copying
内存直接一分为2,把标记对象直接复制向备用区域,然后另一半直接全回收
JVM
内存区域分为新生代young和老生代old
young
eden survivor(to) survivor(from) 8:1:1
当对象创建后进入eden,eden满了会进行minor GC,将存活对象年龄+1并复制放入from,把eden清空
第二次eden又满了,再次minorGC,把eden对象和from对象年龄+1,复制进入to区,清空eden和from,然后from和to名字互换,下一次minorGC再重复
old
当对象age达到一定数字会进入old
如果minorGC时to区域无法容纳所有存活对象,会进入old
比较大的对象,为了减小复制消耗,会进入old
问题:跨代引用——old里有一个map引用了一个young里的object,这样会造成混淆
card-table:把old区分块,有跨代引用的和没有的分开,这样GC时就不需要扫描所有old区
fullGC时,old中通过标记整理法进行回收,因为old中变更频率低