走进JVM

码出高效01之走进JVM

前言:对阿里巴巴出版书籍《码出高效》内容的整理,私以为《码出高效》是一本好书,值得多次翻阅学习。
Java如何实现跨平台?

​ 任何计算机领域问题都可以通过增加一个中间层解决。JVM对Java的字节码解释执行,屏蔽对底层操作系统的依赖,达到一次编写,到处运行。

​ Java所有的指令共200多个,一个字节可以存储256种信息,这样一个字节称为字节码。Java字节码流起始的四个字节非常特殊:“cafe babe”,是一个魔法数,标记这个文件是一个Java文件。

JVM 常见指令
  1. 加载或存储指令
    • 将局部变量加载到操作栈: ALOAD、ILOAD
    • 从操作栈顶存储到局部变量表: ASTORE、ISTORE
    • 将常量加载到操作栈顶:
      • ICONST: 加载 -1 ~ 5 的数
      • BIPUSH:Byte Immediate PUSH,加载 -128 ~ 127 的数
      • SIPUSH: Short Immediate PUSH, 加载 -32768 ~ 32767
      • LDC: Load Constant, 加载 -2147483648 ~ 2147483647 和字符串
  2. 运算指令:IADD、IMUL
  3. 类型转换指令:I2L、D2F
  4. 对象创建与访问指令
    • 创建对象指令:NEW、NEWARRAY
    • 访问属性指令:GETFILED、PUTFILED、GETSTATIC
    • 检查实例类型指令: INSTANCEOF、CHECKCAST
  5. 操作栈管理指令
    • 出栈操作:POP、POP2
    • 复制栈顶元素并压入栈: DUP
  6. 方法调用
    • INVOKEVIRTUAL: 调用对象实例方法
    • INVOKESPECAIL: 调用实例初始化方法,私有方法,父类方法
    • INVOKESTATIC: 调用类静态方法
    • RETURN: 返回VOID类型
  7. 同步方法: MONITORENTER、MONITOREXIT
  8. 其他:LINENUMBER
源代码到字节码过程

JAVA源文件 ——> 词法解析 ——> 语法解析 ——> 语义解析 ——> 字节码

字节码执行方式

​ 字节码通过类加载方式加载到JVM环境,才可以执行。执行方式有三种:

  1. 解释执行:
  2. 编译执行
  3. JIT 编译与解释混合执行
    • 优势:解释器在启动时先解释执行,省去编译时间。随着时间推进,通过热点代码分析,识别高频的方法调用,循环体,公共模块等,基于JIT动态编译技术,将热点代码转换转换成机器码,直接交给CPU执行。

类加载过程:双亲委派模型(Parents Delegation Model)

​ 计算机模型里任何程序都需要加载到内存中,才可以执行。类加载三个步骤:

  1. Load:读取类文件,产生二进制流。
  2. Link:
    • 验证:final 是否合格,类型是否正确等
    • 准备:为静态变量分配内存,设定默认值
    • 解析:确保类与类之间引用正确性,完成内存结构布局
  3. Init:执行类的构造器方法
类加载器如何定位到具体类并加载读取
  1. Bootstrap: 通常由与本地操作系统相关的本地代码实现,是最根基的类加载器,负责加载最核心的JAVA类,比如 Object、String、System等。 <jir/lib/rt.jar>
  2. Extension ClassLoader: 扩展类加载器,加载一些扩展的系统类。比如XML,加密,压缩相关动能类。 <jre/lib/ext/*.jar>
  3. Application ClassLoader: 应用类加载器,主要加载用户自定义的CLASSPATH路径下的类。
ClassLoader c = TestWhoLoad.class.getClassLoader();		// AppClassLoader
ClassLoader c1 = c.getParent();							// ExtClassLoader
ClassLoader c2 = c1.getParent();						// null

低层次的类加载器不能覆盖高层次已经加载过的类。如果低层次类加载器想加载一个未知的类,要向上逐级询问:“我可以加载这个类吗?”,被询问的类加载器会自我两个问题:第一,这个类我加载过了吗?第二,我可以加载这个类吗?只有当所有高层次的类加载器两个问题都回答为否,才可以让当前的类加载器加载这个位置类。

​ 实现自定义类加载器步骤:继承ClassLoader,重写findClass()方法,调用defineClass()方法。


JVM内存布局

  1. Heap (堆区)
  • 内存区域最大,存储几乎所有的对象,是OOM故障最主要的发源地。通过以下参数可以设定堆内存大小: -Xms256 -Xmx1024,堆空间不断扩容和回缩,会形成不必要的系统压力,建议在生产上设置一样大小。
  • 堆分为新生代和老年代,新生代 = 1个Eden区 + 2个Survivor区。
  • 对象分配内存规则:
    • 大部分对象在Eden区生成,当Eden区放不下时,触发YGC。如果Eden还是放不下,则尝试在老年代分配,如果老年代也放不下,则进行FGC,如果依然放不下,抛出OOM。
    • YGC过程:没有被引用的对象直接被清除,存活的对象被送到Survivor区,Survivor区分为S0和S1,每次使用一块空间。YGC时候,将存活的对象复制到未使用的那块空间,然后将正使用的空间完全清除,交换两块空间的使用状态。存活对象有一个计数器,每次YGC的时候计数+1,当超过阈值的后(默认为15),晋升至老年代。如果YGC要移送的对象大于Survivor容量上限,直接移送到老年代。
  1. Metaspace(元空间)
  • 只有在JDK7及之前版本,有Perm区(永久代),在启动时固定大小。FGC时会移动类元信息,如果动态加载的类过多时,容易产生Perm区的OOM。
  • JDK8及以后版本,使用元空间替代永久代,元空间在本机分配内存。
  1. JVM Stack (虚拟机栈) LIFO
  • 是描述Java方法执行的内存区域,每个方法从调用开始到执行结束,就是栈帧入栈到出栈的过程。只有位于栈顶的帧才是有效的,称为当前栈帧;正在执行的方法称为当前方法。方法正常执行结束,会跳到另一个栈帧上。StackOverflowError表示请求栈溢出,导致内存耗尽,常出现在递归方法。
  • 栈帧包括:局部变量表、操作栈、动态连接、方法返回地址等
    • 局部变量表:用来存放局部变量和方法参数的区域。非实例方法,在index【0】存储的是方法的实例引用,随后是参数和局部变量。
    • 操作栈:初始状态为空的桶式结构,方法执行过程中,各种指令往栈中写入和提取信息。
    • 动态连接:每个栈帧包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
    • 方法返回地址:方法退出过程相当于弹出当前栈帧,退出方式有是三种:
      • 返回值压入上层调用栈帧
      • 异常信息抛出给能够处理的栈帧
      • PC计数器指向方法调用后的下一条指令
  1. Native Method Stacks (本地方法栈)
  • 虚拟机栈“主内”,本地方法栈“主外”,为Native方法服务。调用本地方法,会进入不受JVM控制的世界,本地方法可以通过JNI(Java Native Interface)来访问虚拟机运行时的数据区。JNI使Java具有深度使用操作系统的特性功能,复用非Java代码。
  1. Program Counter Register (程序计数寄存器)
  • 寄存器存储指令相关的现场信息,在并发过程中,任何一个确定的时刻,只会执行某个线程的一条指令,势必会导致经常中断和恢复。
  • 每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器存放执行指令的偏移量和行号指示器等,线程的执行或恢复都要依赖程序计数器。

内存区域:

  • 线程共享:
    • 堆 OutOfMemoryError: Java heap space
    • 元空间 OutOfMemoryError: Metaspace
  • 线程私有:
    • 虚拟机栈 StackOverflowError
    • 本地方法栈 StackOverflowError
    • 程序计数器

对象实例化

​ 从简单的 Object o = new Object(); 代码进行分析。

  1. 从字节码的角度进行分析:
stack=2, locals=l, args_size=0
	NEW java/ lang/object
	DUP
	INVOKESPECIAL java/lang/object.<init> ()V
	ASTORE_1
	LocalVariableTable:
	Start    Length    Slot    Name    Signature
	  8        1         0     ref     Ljaval lang/object;
  • NEW:如果找不到class对象,就进行类加载,类加载成功,在堆中分配内存。分配完毕,则进行零值初始化。分配过程中,注意引用是占据存储空间,是一个变量,占4个字节。这个过程完毕,将指向对象的实例变量压入栈顶。
  • DUP:在栈顶复制该变量,这时,在栈顶有两个指向堆内实例对象的引用变量。两个引用变量的目的不同,压至底下的引用用于赋值,或者保存到局部变量表;另一个引用作为句柄调用相关方法。
  • INVOKESPECIAL:通过栈顶的引用变量调用方法。是类初始化时执行的方法,是对象初始化执行的方法。
  1. 从执行步骤的角度分析:
  • 确认类元信息是否存在:首先在metaspace内检查要创建的类元信息是否存在,不存在,则使用类加载器以ClassLoader+包名+类名 为key进行查找对应的.class文件。没找到,抛出ClassNotFoundException;如果找到,则进行类加载,并生成对应的Class类对象。
  • 分配对象内存:计算对象占用空间大小,如果成员变量是引用变量,仅分配引用变量空间,4个字节大小。接着在堆中划分一块内存给对象。
  • 设定默认值:成员变量的值都需要设定为默认值,即不同形式的零值。
  • 设置对象头:设置新对象的哈希码、GC信息、锁信息、对象所属的类元信息等。
  • 执行init方法:初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

垃圾回收

垃圾回收是为了清除不再使用的对象,自动释放内存。

  • 怎么判断对象可以回收:如果一个对象与GC Roots之间没有直接或间接的引用关系,比如:某个失去任何引用的对象,或者两个互为环岛状循环引用的对象。
  • 什么对象可以作为GC Roots:类的静态属性中的引用对象、常量引用的对象、虚拟机栈中引用的对象、本地方法栈引用的对象等。

垃圾回收算法:

  1. 标记-清除 算法:从每个GC Roots出发,依次标记有引用关系的对象,最后将没有标记的对象清除。

    • 缺点:造成大量的空间碎片,分配一个较大连续的空间时,容易引发FGC。
  2. 标记-整理 算法:从每个GC Roots出发,标记存活对象,然后将存活对象整理到空间的一端,形成连续的已使用空间,最后把已使用的空间之外的全部清理掉。

  3. Make-Copy 算法:将空间分为两块,每次只使用其中的一块,把存活的对象复制到另一块未激活的空间,然后将已激活的空间中对象清除,并把激活状态互换。堆内存空间分为较大的Eden和两块较小的Survivor,每次只使用Eden和一块Survivor。

    • 优点:能够并行的标记和整理,减少了空间的浪费。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值