目录
- jvm是什么
- .class加载过程
- 干预.class
- .class文件内容
- 1 加载
- 2-1 连接:验证(class字节流的校验)
- 2-2 连接:准备(分配内存,初始化默认值)
- 2-3 连接:解析
- 3 class 初始化
- 什么时候需要对类进行初始化?
- 附:static执行时机?
- 4. 类实例化new Object()
- 附:`Object o = new Object();`的汇编指令
- 5. 运行时内存(java8为例)
- 程序计数器(PC)
- 虚拟机栈(线程stack,计算逻辑)
- 本地方法栈(native method stack)
- 堆(heap)(存储功能)
- Thread Local Allocation Buffer,线程本地分配缓存
- 方法区->元空间(不属于JVM,使用堆外内存)
- 元空间
- 为什么去掉JDK1.7的永久代
- 对象存储
- `new Object()`占用多少字节?16字节?
- 对象头之markword
- 锁升级过程
- 重点掌握.class到运行时的整个流程
- 掌握运行时内存分布图
- 知道new Object()的对象头信息
后续将分析类加载过程中的常见问题,特别是初始化问题、双亲委派加载问题、运行时各区域的情况。这些都需要专门的例子来详细说明,才能加深理解和记忆。
jvm是什么
JVM组成:
- 类加载子系统
- 运行时数据区(方法区,堆,thread stack, native statck, pc registor等)
- 执行引擎(interpreter, jit compiler, garbage collector; native method interface/Library等)
jvm oracale官方文档
.class加载过程
class文件是一组以8位字节为基础单位的二进制流
,任何一个Class文件都对应唯一一个类或接口的定义信息
类加载过程为:
加载(即加载class文件)=> 连接 ( 验证 =》 准备 =》 解析)=> 初始化=> 使用=> 卸载
干预.class
类似学习Spring框架的BeanFactoryPostProcessor干预BeanDefinition一样,如上的这些技术可以干预.class。 .class不同显然就能代理出不同的东西出来
.class文件内容
文件包含如下信息:
1 加载
即由类加载器(ClassLoader)执行,通过一个类的全限定名
来获取其定义的二进制字节流
(Class字节码),将这个字节流所代表的静态存储结构转化为运行时(Runtime data area)区域的入口,根据字节码在Java堆
中生成一个代表这个类的java.lang.Class
对象。
2-1 连接:验证(class字节流的校验)
验证是连接阶段的第一步,这一步主要的目的是确保class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
2-2 连接:准备(分配内存,初始化默认值)
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
public static int value = 12;
变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic
指令是程序被编译后,存放于类构造器<clinit>()
方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。
相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,例如上面类变量value定义为:
public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。
2-3 连接:解析
解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
解析后的信息存储在ConstantPoolCache类实例中,如
- 类或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
3 class 初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>
方法的过程(注意<clinit>
不是类的构造函数)。
clinit
方法是由编译器自动收集类中的类静态变量的赋值操作和静态语句块中的语句合并而成的。JVM会保证clinit
方法执行之前,父类的clinit
方法已经执行完毕。
什么时候需要对类进行初始化?
- 使用
new
该类实例化对象的时候 - 读取或设置
类静态字段
的时候(但被final修饰的字段,在编译时就被放入常量池;连接-准备阶段会赋予变量常量值;所以(static final
)的静态字段除外) - 调用
类的静态方法
的时候 - 使用反射
Class.forName("xxx")
对类进行反射调用的时候,该类需要初始化; - 初始化一个类的时候,有父类,
先初始化父类
(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化); - 被标明为启动类的类(即包含
main()方法
的类)要初始化; - 当使用JDK1.7的动态语言支持时,如果一个
java.invoke.MethodHandle
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上情况称为对一个类进行主动引用,且有且只要以上几种情况是需要对类进行初始化:
-
所有类变量静态初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是
<clinit>
方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用; -
编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
-
如果超类还没有被初始化,那么优先对超类初始化,但在
<clinit>
方法内部不会显示调用超类的<clinit>
方法,由JVM负责保证一个类的<clinit>
方法执行之前,它的超类<clinit>
方法已经被执行。 -
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其它线程。(所以可以利用静态内部类实现线程安全的单例模式)
-
如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类
<clinit>
方法;
附:static执行时机?
static块的执行发生在类"初始化"的阶段(注意不是类实例初始化过程)。类初始化阶段,jvm会完成对静态变量的初始化,静态块执行等工作。
是否执行static块的几种情况:
-
第一次
new A()
会;因为这个过程包括了初始化 -
第一次
Class.forName("A")
会;因为这个过程相当于Class.forName("A",true,this.getClass().getClassLoader())
;
@CallerSensitive
public static Class<?> forName(String className)throws ClassNotFoundException {Class<?> caller = Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
-
第一次
Class.forName("A",false,this.getClass().getClassLoader())
不会。因为false指明了装载类的过程中,不进行类初始化;没有类初始化,则不会执行static块。 -
类似
getSystemClassLoader().loadClass("com.other.Hello");
也不会。
/*** Loads the class with the specified <a href="#name">binary name</a>.* This method searches for classes in the same manner as the {@link* #loadClass(String, boolean)} method. It is invoked by the Java virtual* machine to resolve class references. Invoking this method is equivalent* to invoking {@link #loadClass(String, boolean) <tt>loadClass(name,* false)</tt>}.** @param name* The <a href="#name">binary name</a> of the class** @return The resulting <tt>Class</tt> object** @throws ClassNotFoundException* If the class was not found*/
public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}
4. 类实例化new Object()
当用new XXX()
创建对象时,首先在堆上为对象分配足够的存储空间
这块存储空间会被清零,这就自动地将对象中的所有基本类型数据都设置成了缺省值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null。
执行所有出现于字段定义处的初始化动作(非静态对象的初始化)
然后执行构造器
附:Object o = new Object();
的汇编指令
Object o = new Object();
的汇编指令
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return
隐含一个对象创建的过程:(记住3步,不是原子操作)
- 堆内存中申请了一块内存(new指令)【半初始化状态,成员变量初始化为默认值】
- 这块内存的构造方法执行(invokespecial指令)
- 栈中变量建立连接到这块内存(astore_1指令)
5. 运行时内存(java8为例)
程序计数器(PC)
- 指向下一条需要执行的字节码;记录当前线程的位置便于线程切换与恢复;
- 唯一 一个不会出现
OOM
的区域
虚拟机栈(线程stack,计算逻辑)
描述了Java方法执行的内存模型,创建栈帧,保存该本地方法的局部变量表、操作数栈、动态链接、出口信息。
- Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
- 栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息
- 每一个方法被调用直至执行完毕的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程
本地方法栈(native method stack)
描述native
方法执行,会创建栈帧(本地方法栈):也保存了该本地方法的局部变量表、操作数栈、动态链接、出口信息。
native会调用本地方法库的本地方法接口(JNI:Java Native Interface)
能允许JAVA程序调用C/C++写的程序,扩展性功能
堆(heap)(存储功能)
主要用于存放对象
;Java8之前有【方法区】的大部分被移到堆中了,所以,堆中还放有:运行时常量池
,字符串常量池
Thread Local Allocation Buffer,线程本地分配缓存
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域TLAB(Thread-local allocation buffer),TLAB也仅作用于新生代的Eden Space。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有,所以没有锁开销。也就是说,Java中每个线程都会有自己的缓冲区称作TLAB,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
方法区->元空间(不属于JVM,使用堆外内存)
静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中
-
方法区主要存放的是 Class,而堆中主要存放的是实例化的对象
-
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域
-
方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
-
方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
-
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:
java.lang.OutofMemoryError:PermGen space(JDK7及之前)或者java.lang.OutOfMemoryError:Metaspace(JDK8及之后)
元空间
类的元数据:如方法、字段、类、包的描述信息,这些信息可以用于创建文档、跟踪代码中的依赖性、执行编译时检查
线程栈中要new对象,从元空间能获取到class信息,然后在堆中分配内存,并与栈中变量建立引用关系
Metaspace由两大部分组成:Klass Metaspace和NoKlass Metaspace。
- klass Metaspace就是用来存klass的,就是class文件在jvm里的运行时数据结构,是一块连续的内存区域,紧接着Heap
- NoKlass Metaspace专门来存klass相关的其它的内容,比如method,constantPool等,可以由多块不连续的内存组成
为什么去掉JDK1.7的永久代
永久代是方法区的实现,使用堆内存,不好分配大小。JVAVA 8开始,使用元空间取代了永久代。JDK 1.8后,元空间存放在堆外内存中(因此,默认情况下,元空间的大小仅受本地内存限制)。
gc问题,OOM,应用越来越大(调优不友好)
- JVM加载的Class的总数,方法的大小等都很难确定,因此对永久代大小的指定难以确定。太小的永久代容易导致永久代内存溢出,太大的永久代则容易导致虚拟机内存紧张。
- ASM,Cglib动态生成,也导致永久代大小的指定难以确定
对象存储
- 对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
hotspot 相关术语表
术语 | 英文说明 | 中文解释 |
---|---|---|
mark word | The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits. | 用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等 |
klass pointer | The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”. | 是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。 |
new Object()
占用多少字节?16字节?
java -XX:+PrintCommandLineFlags -version-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
- +UseCompressedClassPointers:64bit机器,一个指针8个字节,如果使用压缩会只有4个字节
- -XX:+UseCompressedOops:普通对象指针,如果压缩也是4个字节
public class Main {public static void main(String[] args) throws Exception {Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());}
}
/*
java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalmarkword:8个字节
_kclass:4个字节
没有成员变量:instance data:0字节
紧接着4个字节,是对齐要使用4个字节,即凑成8个字节即共16个字节
*/
对象头之markword
markword共8个字节,64bit,包括:锁信息,gc信息,identity hashcode
- 无锁例子
public class Main {public static void main(String[] args) throws Exception {Object o = new Object();int hashCode = o.hashCode();int b = hashCode % 2;System.out.println(hashCode + " " + Integer.toBinaryString(hashCode) + " " + b);System.out.println(ClassLayout.parseInstance(o).toPrintable());}
}
/*2007328737 1110111101001010110011111100001 1
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 e1 67 a5 (00000001 11100001 01100111 10100101) (-1519918847)4 4 (object header) 77 00 00 00 (01110111 00000000 00000000 00000000) (119)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalmarkword 64bit,如下00000000 000000 00000000 01110111 10100101 01100111 11100001 00000001根据无锁(new),64bit 具体如下unused:25bit | identity hashcode:31bit |unused | age | biased_lock | lock
00000000 000000 00000000 0 | 1110111 10100101 01100111 11100001 | 0 | 0000 | 0 | 01| | | | |
*/
锁升级过程
无锁态(new) =》 偏向锁 =》 轻量级锁,自旋锁,无锁 =》 重量级锁