本文主要内容
- JVM组成
- 垃圾回收机制简介
本文不涉及的内容
- 编译技术
- JVM中class文件结构
- JVM线程同步机制
- JVM指令集构成
- 类加载流程及其生命周期
- 对象实例化过程
- 对象在堆中的表示方法
这些以后慢慢填吧
JVM与我们的关系
广义上来讲,JVM指的是一种设计规范
jvm_1.png从图中我们可以看到,Java编译器将.java文件编译成class文件,交给JVM解析和执行。只要满足这个要求的我们都可以称其为Java虚拟机
JVM构成
JVM整体结构可以由下图展示
jvm_2.png其中,当JVM加载类文件时,就会将类的相关信息放在方法区(method area)中,程序执行过程中所有对象会放在堆中
jvm_3.pngClass loader subsystem
整个class loader system大致如下图
jvm_4.pngClass loader分为两类
- Primordial class loader 这个与JVM实现有关,有且仅有一个
- class loader 对象,不同的classloaders加载的类会放在不同的命名空间中(不同的 classloader 加载同一个类不会被 JVM 认为是同一个类
class loader 的对象和 Class 的对象都在堆中,加载的类型的数据在方法区。
每一个 class loader对象维护一个自己的命名空间,因此一个完整的类名是不足以证明该类的唯一性的-->热更新的手段之一。
class loader的设计是java实现动态加载类的基础-->安卓插件化技术
Method Area
在JVM中,加载的类型(类)是存放在方法区的。所有线程都共享方法区
JVM对于每种类型都会存储一系列的信息:
- 类的全名
- 类的直接父类(除非是接口或java.lang.Object)
- 是类还是接口
- 类的标识符(public, abstract, final)
- 实现的接口的list
- 类的常量池(constant pool)
- 成员变量信息
- 方法信息
- 类变量信息(Class Variables)
- Classloader 引用(加载此类的class loader)
- 一个Class类实例引用
类的常量池是一个有序表,持有所有的常量(字符串,整数,浮点数),以及其他类,作用域,方法的符号引用。是Java程序实现动态链接的关键部分
方法信息存储了方法名,方法返回的类型,参数表,方法的修饰符(public, private, protected, static, final, synchronized, native, abstract)
如果方法不是抽象方法,那么还会额外存储方法的字节码,异常表,操作数栈大小和本地变量
类变量(Class Variables) 就是存储了和类相关的变量(static),但有final修饰时情况不同,final static 修饰的变量同时会拷贝一份存储在任何其他使用它的类的数据里,并且是在编译期完成的复制(compile time vs runtime)
当然很多JVM在方法区会加入其他的数据结构来加快执行速度
下面举一个例子:
class Lava {
private int speed = 5;
void flow() {
}
}
class Volcano {
public static void main(String[] args) {
Lava lava = new Lava();
lava.flow();
}
}
下面展示的是JVM一种处理方法区的手段。
首先,你将Volcano
这个名字告诉给JVM,JVM找到这个名字对应的类文件Volcano.class
,JVM读取这个类然后将信息加载到方法区。虚拟机之后调用main函数,即解释方法区中main方法的字节码。此时Lava类还没有被加载,于此同时,虚拟机会维护一个指针指向当前的类常量池(Volcano)
main方法中首先请求新建一个Lava实例,JVM会从Volcano这个类的常量池的第一个元素开始遍历,找到一个指向Lava
这个类的符号引用。此时JVM会检查Lava
这个类的信息有没有加载进方法区。显然Lava类还没有加载进来,因此它会去寻找并读取Lava.class
,将信息提取到方法区中。
之后,JVM会将刚才Volcano
中常量池的符号引用,字符串"Lava",替换为一个指向Lava类信息的指针。那么之后JVM如果再次碰到了使用Lava的情况,就会直接使用这个指针去访问Lava类的信息。这个过程被称为constant pool resolution.
直到这里,JVM才开始为新的Lava
对象分配内存。这里刚才指向Volcano类常量池的指针就会指向Lava类常量池上,用来计算一个Lava对象需要的空间,JVM总是能够在根据方法区中一个类的信息判断其对象需要的空间
一旦创建好了对象,虚拟机就会对对象中的属性(speed)设置初始值(包括父类),之后在栈帧上压入一个该对象的引用(lava),之后的方法调用就是利用这个引用去调用初始化方法(5),之后的flow()方法调用和上面的流程类似
PC & Stack & Stack Frame
JVM是一个栈虚拟机,它并不使用寄存器保存中间值,而是使用栈帧来保存。每当创建一个新的线程,该线程就会拥有一个自己的PC计数器和Java栈,PC用来指示下一条待执行的指令
Java栈是由栈帧(Stack Frame)构成的,每当线程调用一个方法,JVM就会将一个栈帧压栈,当方法返回后,这个栈帧弹出。所有在栈里的数据都只对持有该栈的线程
jvm_5.png栈帧由三个部分组成:
- 本地变量
- 操作数栈
- 栈帧数据
本地变量包含一个方法的参数和本地变量,例如:
class Example3a {
public static int runClassMethod(int i, long l, float f, double d, Object o, byte b) {
return 0;
}
public int runInstanceMethod(char c, double d, short s, boolean b) {
return 0;
}
}
这段程序的栈帧中本地变量如图所示
jvm_6.png注意下runInstanceMethod方法中第一个变量是一个reference类型,其实这就是一个this
引用,对每个实例方法都会隐式地传入这个引用
最后,所有对象在Java中都是引用传递,对象本体存在于堆中,你绝不可能在本地变量,操作数栈中找到真实的对象,你只能拿到对象的引用
操作数栈也是一个栈,与本地变量采用下标访问不同,操作数栈只有出栈入栈两种操作
比方说下面几个指令:
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
的执行过程如下图所示
jvm_7.png栈帧数据(Frame Data)包括了对constant pool resolution
,方法正常返回,异常处理的支持
JVM中有大量的指令集是和常量池相关的,凡是这样的指令虚拟机都是使用栈帧数据中的指针去获取对应的信息,之前提到过这些刚开始都是符号,当遇到符号时就会开始执行constant pool resolution
栈帧会持有对方法异常表的引用
图中同时还出现了一个本地方法栈,JVM并没有规定本地方法栈中的状态表示。但是可以看到它是独立于Java栈帧的。
堆
所有线程共享一个堆。没有任何办法在两个JVM实例之间共享堆,下面是一个值得大家思考的问题
如何实现安卓进程间通信呢?(当然我想有很多人知道是如何实现的,不过这也是思考安卓系统架构缘由的好方式)
垃圾回收算法
现在的回收算法仍旧需要做两件事情:检测所有需要回收的对象;腾出对象空间并提供给程序使用
检测需要定义一组根节点和可达性这个概念,检测引用则有引用计数和追踪两种方式
现在的回收机制是基于Copying Collectors
发展而来的Generational Collectors
Copying Collectors
将所有存活的对象移动到一个新的区域,常见的算法是”stop and copy”算法,堆会被分为两个区域。只有一块区域在特定时刻才会被使用,这块区域会一直被使用直到空间耗尽,此时程序会停止运行,然后堆进行遍历,存活对象会被拷贝到另一片区域,之后程序恢复执行。如此循环反复
Generational Collectors(当代回收算法)
简单的stop and copy算法面临的问题是所有的存活对象每次回收都需要拷贝,改进这种算法的方式基于如下两个事实:
1.绝大部分对象只有很短的生命周期
2.有些对象会有很长的生命周期
Generational Collectors将对象按照年龄分类同时相比老的对象更频繁的回收新对象,这种方式中,堆被分成两个及以上的区域,每个区域代表“一代”对象,最年轻的一代回收的最为频繁,一旦某个对象经过几次回收后仍旧没有被回收,那么它就会变老进入下一代(进入另一个堆块中),当然现在的回收算法远比这个复杂,会有更多的堆划分,更优秀的并发GC支持。比方说这样:
jvm_8.png最后留给大家一个小问题思考下吧
为什么Android自定义View中onDraw方法不推荐创建对象呢?
最后附上例子中Volcano
类和Lava
类的常量池和栈帧
aLIEzTeddeMacBook-Pro:~ JianGuo$ java -version
java version "1.8.0_77"
Java(TM) SE Runtime Environment (build 1.8.0_77-b03)
Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode)
aLIEzTeddeMacBook-Pro:java JianGuo$ javap -v -verbose Volcano.class
Classfile /Users/JianGuo/IdeaProjects/HelloMac/src/main/java/Volcano.class
Last modified Dec 1, 2016; size 311 bytes
MD5 checksum 1083bc1bf517460255a98c86e6290a69
Compiled from "Volcano.java"
public class Volcano
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // Lava
#3 = Methodref #2.#15 // Lava."<init>":()V
#4 = Methodref #2.#17 // Lava.flow:()V
#5 = Class #18 // Volcano
#6 = Class #19 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Volcano.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 Lava
#17 = NameAndType #20:#8 // flow:()V
#18 = Utf8 Volcano
#19 = Utf8 java/lang/Object
#20 = Utf8 flow
{
public Volcano();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Lava
3: dup
4: invokespecial #3 // Method Lava."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method Lava.flow:()V
12: return
LineNumberTable:
line 6: 0
line 7: 8
line 8: 12
}
SourceFile: "Volcano.java"
aLIEzTeddeMacBook-Pro:java JianGuo$ javap -v -verbose Lava.class
Classfile /Users/JianGuo/IdeaProjects/HelloMac/src/main/java/Lava.class
Last modified Dec 1, 2016; size 267 bytes
MD5 checksum 83e64490eb9647ad12e2525489e5e344
Compiled from "Lava.java"
public class Lava
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#15 // Lava.speed:I
#3 = Class #16 // Lava
#4 = Class #17 // java/lang/Object
#5 = Utf8 speed
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 flow
#12 = Utf8 SourceFile
#13 = Utf8 Lava.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = NameAndType #5:#6 // speed:I
#16 = Utf8 Lava
#17 = Utf8 java/lang/Object
{
public Lava();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #2 // Field speed:I
9: return
LineNumberTable:
line 4: 0
line 5: 4
void flow();
descriptor: ()V
flags:
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
}
SourceFile: "Lava.java"
Reference
Inside the java virtual machine
Understanding Java Garbage Collection
How many types memory areas allocated by JVM
这是我来到简书的第一篇文章,如果有错误的话请在评论区指出,谢谢阅读!
网友评论