一个简单的汇编程序由指令和数据组成,编译为2进制机器码,人和机器都无法区分指令或是数据。
操作系统加载到内存后,通过约定的头部字节,配合特定的寄存器才能标识并正确的运行。
标识指令的寄存器有CS:IP;标识数据的寄存器有DS:XX;标识段寄存器有SS:SP。
如果当前代码遇到中断指令或跳转指令(函数地址),需要保留现场,执行完成之后还原现场。
C有跳转指令,更高级的是通过函数名或函数指针来调用子程序。
java指令到机器指令的转化是利用了函数指针来实现的。
java源代码(.java)被javac编译为字节码(.class),使用java命令加载到jvm中,
实施不同的内存区域管理,配合jvm指令,翻译到对应的机器码。jvm指令200多个。
jvm内存又称为Run-Time Data Area分为两类
1:线程共享区域(JVM启动时创建)(java没有类似c的free函数,需要垃圾收集器管理)
1.1 java堆(java Heap)存放Object和数组。Xms初始堆空间,Xmx最大堆空间
1.2 方法区(Method Area)别名非堆Non-Heap或永久区。存储java加载的类信息、常量、静态变量、方法字节码等等。
1.3 直接内存(Direct Memory) 直接向OS申请的内存区域,速度优于Java堆。(NIO库使用直接内存)
2:线程私有区域(创建线程时创建)
2.1 pc(program counter)寄存器,理解为类似CS:IP的jvm指令地址。如果执行native方法,值为Undefined。
2.2 JVM Stack (jvm栈)和线程执行有关,Xss 线程栈空间。保存的数据结构称为栈帧(frame),由local variable(本地变量表)、operand stack(操作数栈)、runtime constant pool(运行时常量池,方法区)
2.3 Native Method Stack(本地方法栈)别名C Stack
ClassFile结构如下: {
u4 magic; //魔数 0xCAFEBABE
u2 minor_version; //次版本号
u2 major_version; //主版本号 java8为52
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags; //指出class文件定义的是类还是接口,访问级别是public还是private,有18个种类
u2 this_class; //常量池索引,类名,类似完全限定名(.换成/)
u2 super_class; //常量池索引,超类名,类似完全限定名(.换成/)
u2 interfaces_count; //接口计数器,标识索引表的容量
u2 interfaces[interfaces_count]; //接口索引表
u2 fields_count; //字段计数器
field_info fields[fields_count]; //字段表
u2 methods_count; //方法计数器
method_info methods[methods_count]; //方法表
u2 attributes_count; //属性计数器
attribute_info attributes[attributes_count]; //属性表
}
总结:接口索引表、字段表、方法表、属性表都会指向常量池中的某项,常量池也会指向其某项。
可以通过工具查看
1:javap,自带的反编译工具 javap -p xxx.class 或 javap -p -v xxx.class
2:jclasslib https://github.com/ingokegel/jclasslib
3:classpy https://github.com/zxh0/classpy
字节码格式要变为本地机器码才能被执行,JVM通过以下步骤来实现这个变化。
加载---【验证---准备---解析(不一定)】(统称为linking)---初始化---使用---卸载
一、加载
1:通过类的全限定名来获取定义此类的二进制字节流(不一定是class文件)
2:将constant_pool转化为 方法区(Method Area)别名(非堆)Non-Heap或永久区的运行时数据结构
3:在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区数据的访问入口
详细过程参考《java核心技术 卷二》第9章
参考:https://blog.csdn.net/briblue/article/details/54973413
二、验证 确保二进制节流包含的信息符合JVM的要求
1:文件格式验证
2:元数据验证(语义分析)
3:字节码验证(方法体校验分析)
4:符号引用验证(外连接是否正常)
三、准备
1:初始化类变量 static field
2:初始化常量 static final
四、解析是将constant_pool内的符号引用替换为直接引用的过程。
符号引用理解为一个胖子,直接引用理解为13号(jvm中有上下文的具体目标)
如果没有13号呢?加载
五、初始化 执行类构造器clinit方法的过程。
1:clinit由static field和static{}块构成,如果类没有静态域或静态块,jvm可以不生成clinit方法,保证clinit线程安全。
2:clinit方法与类的构造函数<init>() 不同。构造函数在new时
3:父类的clinit方法先执行。
JVM内存分配过程
java类尚未被解析,直接进入慢分配。
如果没有开启栈上分配或不符合条件进行TLAB分配
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。
TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
如果TLAB分配不成功,尝试在eden区分配(新生代)
如果eden区分配失败,则进入慢分配流程
如果对象满足了直接进入老年代的条件,就直接分配在老年代
快速分配。对于热点代码,如果开启逃逸分析,JVM会执行栈上分配或标量替换等优化方案。
垃圾回收示意
在垃圾回收算法中,有一个算法称之为复制算法。其基本思想是把内存分为大小相等的2块,每次只在其中一块区域内进行内存分配,当发生GC时,将这块区域内存活的对象复制到另外一个区域内,然后对这块区域进行Full GC,这样可以保证内存的连续性,减少了空间碎片的产生.然后这种方式牺牲了一半的内存使用空间。Eden区与Survior区的概念也由此得来。
1、Eden区
Edeb区位于JVM中的新生代,是新对象分配内存的地方,由于堆是所有线程共享的,所以在堆上分配内存需要加锁。
而Sun JDK为了提升效率,会为每个新建的线程分配一个独立的内存区域,这块区域称之为TLAB(Thread Location Allocation
Buffer).在TLAB上分配内存是不需要进行加锁的,所以Eden区域的对象内存分配会优先在TLAB上进行.若是对象过大或者是TLAB的内存空间使用完,则对象的内存分配会在堆上进行。如果Eden区内存耗尽,则会触发Minor GC(Young GC)。
2、From Survivor和To Survivor区
针对新生代对象"朝夕生死"的特点,将新生代划分为3块区域u,分别为Eden、From Survior、ToSurvior,比例为8:1:1。
From和To是相对的,每次Eden和From发生Minor GC时,会将存活的对象复制到To区域,并清除内存。To区域内的对象每存活一次,它的"age"就会+1,当达到某个阈值(默认为15)时,ToSurvior区域内的对象就会被转移到老年代。
可以通过设置参数-XX:MaxTenuringThreshold来设置晋升的年龄。
虚拟机提供了一个参数:-XX PertenureSizeThreshold 使得大于这个参数的对象直接在老年代中分配内存,这样就避免了在Eden区域以及Survior区域进行大量的内存复制。
3、老年代
老年代中是存活时间久的大对象(很长的字符串或者是数组),因此老年代使用标记-整理算法。当老年代容量满的时候,会触发一次MajorGC (FullGC)
---------------------
参考:https://blog.csdn.net/weixin_30300689/article/details/79888642
参考:http://www.cnblogs.com/QG-whz/p/9636366.html
参考《实战java虚拟机 jvm故障诊断与性能优化》
参考《深入理解java虚拟机 jvm高级特性与最佳实践》
参考《揭秘java虚拟机 jvm设计原理与实现》
参考《自己动手写jiava虚拟机》