JVM Basic
ClassFile
Java文件经过javac编译后,得到.class字节码文件,再放在JVM上面跑。ClassFile是一种结构规范,所有.class文件都应该符合这样的格式。
基本信息
- magic number:0xCAFEBABE,每个.class必须由这4个字节开头
- 版本号,代表javac的版本
- access flag
常量池
一共有14种类型的常量,分为literal(字面量)和symbolic references.
literal指的是字符串,final常量等。symbolic references指的是,方法名/字段名,他们会指向一个字符串,也就是literal。
javap -v Main.class temp.txt 查看常量池的信息
Fields, Methods, Attributes
以methods为例,这个表里面会记录access_flag(public/static/volatile), 对常量池的引用(方法名字string,方法descriptor string)
类的生命周期
加载、连接(验证、准备、解析)、初始化、使用、卸载
加载
- 类的加载器把.class文件以字节流的形式,通过不同渠道获取字节码的信息,这个操作是根据类的全限定名定位的。全限定名:package name+class name。“不同渠道”指的是动态代理也可以生成一个类,这个类也有可能是本地磁盘上的类。
- 把字节流的信息转化为一个instanceKlass的C++对象,并且把它放在内存中的方法区。这个对象包含了这个类的一些基本信息。
- 在堆里面生成一份java.lang.Class对象,这是一个Java对象。
验证
- 文件格式:版本号、magic number. 环境的版本号不能低于该类的版本号。
- 元数据:确保这个类是有父类的,java.lang.Object
- 字节码验证:通过分析control flow/data flow, 确保semantic正确。
- 符号引用:常量池中有许多符号引用,确保这些引用是正确的、有权限的
准备
给static变量初始值:0. 如果是final static变量,则给它所生命的值。
解析
把常量池内的符号引用换成直接引用,使用实际的内存地址去访问那个数据。这样得到运行时常量池,存在于方法区中。
初始化
对于一个类,在它classFile的Methods区,会有一个默认
这个阶段给static variable赋值,并且执行静态代码块。如图所示,首先把1放在操作数栈里面,接着把1赋给堆区里的常量池的静态变量。
导致类的初始化的方式:
- 访问一个类的 static variable/method。(除了final修饰的以外)。
- new一个类的时候
- 调用java.lang.reflect对类进行反射调用的时候
- 执行main方法的当前类
打印:DACBCB。类的加载阶段打印D,执行main打印A;创建Test()的时候先执行构造代码块,再执行构造函数。每次new一个对象的时候,构造代码块都会执行一次。
静态代码块、构造代码块、构造函数执行顺序
此时会输出 2.
执行 main,new了B类,所以首先初始化父类A,a值为1;接着初始化B,a值为 2.
如果去掉new B02()这一行,则直接输出1. (直接访问父类的静态变量,不会触发子类的初始化)
类加载器
ClassLoader是一个抽象类,每一个 java类都有一个引用,指向加载它的类加载器。
分类
Java 8以及之前,分为启动类加载器(Bootstrap ClassLoader), 扩展类加载器(Exstension)
Bootstrap ClassLoader
由C++实现,如果用java代码获取会显示null,用来加载JDK内部的核心类库。
/Users/ruoke/Library/Java/JavaVirtualMachines/corretto-1.8.0_412/Contents/Home/jre/lib
加载这个目录下的类。也可以通过-Xbootclasspath参数指定想要被加载的jar包的路径。
Exstension ClassLoader
JDK内部提供的、使用Java 实现的静态内部类,加载的路径:
JavaVirtualMachines/corretto-1.8.0_412/Contents/Home/jre/lib/ext
Application ClassLoader
包含 maven本地仓库里面那些jar包、自己编写的 Java 类
双亲委派机制
决定某个 Java 类到底由哪个类加载器来加载。
从下到上,判断某个加载器是否加载过这个类。如果加载过,就直接返回。如果没有加载过,则从启动类加载器开始尝试加载这个类,如果失败,则一步步往下尝试。
优点
- 保证java核心类不被篡改,无法使用自己定义的,以"java.“开头的类
- 防止类重复。如果有两个类加载器加载了同一个类限定名的类,他们会被认作是两个不同的类,造成重复。
String类能被覆盖吗? 不能。如果自己定义了String类,那么从下往上判断某个加载器是否加载过,发现启动类加载器加载了String类,它把这个加载好的类进行返回。
打破双亲委派机制:自定义类加载器
|
|
在findClass里面调用 defineClass函数,能将字节流保存为方法区的instanceKlass对象和堆上的instance对象。
如果想打破双亲委托模式,则需要重写loadClass。如果不想打破,重写findClass即可。 自定义类加载器的父类是应用程序加载器。
在java中,只有相同类加载器&相同的类限定名才能被认为是同一个类。
loadClass:
实现双亲委派机制的完整流程
调用 findLoadedClass 检查类是否已加载
先委托父加载器尝试加载
父加载器失败后才调用 findClass
运行时数据区
所有线程共享:堆、方法区
每个线程私有:虚拟机栈、本地方法栈、程序计数器
本地方法栈
在Hotspot栈空间中,本地方法栈的栈帧和Java 虚拟机栈的栈帧是放在一起的
虚拟机栈
栈帧
- 操作数栈(用来存放临时变量&计算的中间结果)
- 本地变量表:实例方法的this对象、方法参数、方法体内声明的局部变量。
- 方法返回地址
- 异常表:异常捕获的生效范围、异常发生后跳转到的字节码指令位置。
- 动态链接:作用于一个方法调用另一个方法的时候。这时候需要对于方法名的符号引用,转变为对于方法的内存地址的直接引用。
栈内存溢出
JVM 创建栈会按照默认的大小,在 Linux X86(64 位)上,栈的默认大小是1MB。也可以通过参数来指定栈的大小。实际工作中,设置为 256k就足够了。
StackOverFlowError: 如果栈的内存不可以动态扩展,线程请求栈的深度(栈帧个数)超过当前Java虚拟机栈的最大深度
OutOfMemoryError: 栈的内存可以动态扩展,但是虚拟机在动态扩展栈的时候无法申请到足够的内存
程序计数器
保存当前执行的字节码指令的行号(偏移量)。当字节码解释器区解释字节码的时候,它会根据方法区的起始地址和指令的偏移量来计算得到该指令存储的位置。
堆
堆里面包含一切对象的实例和数组。
|
|
堆内存相关有三个值:used, total, max. total表示
|
|
启动arthas
memory
Memory used total max usage
heap 667M 1786M 2304M 28.96%
随着堆上的 object 越来也多,堆内存used越来越多,会渐渐逼近total,这时候虚拟机会给堆分配更多内存。虚拟机最多给堆分配max内存。当 used接近max时,会有异常:java.lang.OutOfMemoryError: Java heap space。
max的默认值是系统内存的1/4, total的默认值是系统内存的 1/64。可以通过虚拟机参数修改。
对象的分配是在Eden区,在经历过一次垃圾回收过的对象会去S0或S1,并且年龄为1。在默认情况下,年龄到15的对象去老年区。可以通过参数来设置去老年代的年龄。不过只能设置到0-15(二进制 1111)之间。
新生代与老年代的默认比例是1:2。
新生代内部又分为 Eden 区 和 Survivor 区,默认比例为 8:1:1。
方法区
方法区是一个抽象概念,永久代和元空间则是方法区的具体实现。
方法区存放的是 instanceKlass对象。
- 类的元数据(class metadata):类名、父类名、方法信息、字段信息
- 运行时常量池:存储Class File常量池里的内容。包含字面量和引用。数值型的字面量直接存储在常量池中,但String常量存储在堆中的字符串常量池中,运行时常量池仅仅包含对它们的引用。符号引用在解析阶段会变成直接引用,也就是直接指向某个对象的内存地址。
- 静态变量。这是类的信息,而不是属于某个实例对象的信息,因此存在于方法区。
- 方法代码:存储JIT编译后的本地代码或解释执行的字节码
JDK7及之前
方法区存在于堆内的永久代空间,永久代的内存有上限。永久代的初始内存和最大内存都可以通过虚拟机参数来设置。
JDK8之后
方法区存在于MetaSpace中,MetaSpace存在于操作系统管理的直接内存中。
访问直接内存使用NIO(New-IO)包,这是一种非阻塞的IO,基于channel和buffer。我们通过DirectByteBuffer对象,作为对直接内存的引用,来操作直接内存。
访问直接内存的速度快于访问Java堆,因此直接内存适合放读写频繁的对象。