一,JVM 的主要组成部分及其作用
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载): 根据给定的全限定名类名(如: java.lang.Object)来装载class文件到Runtime data area中的method area。
Execution engine (执行引擎) : 执行classes中的指令。
Native Interface(本地接口): 与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域): 这就是我们常说的JVM的内存。作用: 首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区 (Runtime data area) 的方法区内,而字节码文件只是JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎 (Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口 (Native lnterface)来实现整个程序的功能。
下面是Java程序运行机制详细说明
Java程序运行机制步骤
首先利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;再利用编译器(javac命令)将源代码编译成字节码文件,字节码文件的后缀名为.class;运行字节码的工作是由解释器(java命令)来完成的。
从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些,.class文件加载到JVM中。其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
二,堆栈的区别
官方文档中有这么一句话
翻译成中文就是:java 虚拟机是一种抽象的计算机。
既然是计算机就需要遵循冯诺依曼的计算机体系模型。
类加载器相当于输入设备,执行引擎相当于输出设备,堆和方法区相当于存储器,栈相当于中央处理器。
物理地址角度
堆的物理地址分配对对象是不连续的。因此性能慢些。
在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,标记-复制,标记-压缩,分代 (即新生代使用标记-复制算法,老年代使用标记-压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存角度
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。
存放的内容角度
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储。
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
程序的可见度角度
堆对于整个应用程序都是共享、可见的。
栈只对于线程是可见的。所以也是线程私有,他的生命周期和线程相同,
三,JVM类加载机制的特性
- 全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
例如,系统类加些器AppClassLoader加想入口类 (含有main方法的类)时,会把main方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加裁器就是入口类加载器。
以上步要只是调用了ClassLoaderloadClass(name)方法,并没有真正定义类,真正加载class字节码文件牛成Class对象由“双亲委派”机制完成。
- 父类委托
“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载咒加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
父类委托别名就叫双亲委派机制。“双亲委派”机制加载Class的具体过程是
1.ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器,
2. 父类加载器判断是否加载过该Class,如果已加载,则这回Class对象,如果没有则委托给祖父类加载器。
3.依此类推,直到始祖类加载器 (引用类加载器)。
4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类
三,对象的生命周期
1,创建阶段
(1)为对象分配存储空间
(2)开始构造对象
(3)从超类到子类对static成员进行初始化
(4)超类成员变量按顺序初始化,递归调用超类的构造方法
(5)子类成员变量按顺序初始化,子类构造方法调用,并且一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
2,应用阶段
(1)系统至少维护着对象的一个强引用(Strong Reference)
(2)所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、
弱引用(Weak Reference)或虚引用(Phantom Reference))
引用的定义:
1.我们的数据类型必须是引用类型
2.我们这个类型的数据所存储的数据必须是另外一块内存的起始地址
3,不可见阶段
不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。程序不再持有对象的强引用。(但是某些类的静态变量或者JNI是有可能持有的 )
4,不可达阶段
指对象不再被任何强引用持有,GC发现该对象已经不可达
与不可见阶段的区别就是:不可达是经过算法之后的
那么有哪些算法?
引用计数:
对象有一个引用就+1,去掉一个引用就-1,当引用数为0就认为是没有引用,可以回收
问题:解决不了循环依赖问题
可达性分析:通过GC Root的对象,开始向下寻找,看某个对象是否可达什么能看作GC ROOT:加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等本质上就是一组活跃的引用
5,收集阶段
看对象需不需要执行finalize方法:
如果不需要:虚拟机已经调用过finalize方法或者对象没有重写finalize方法,那么对象可以回收
需要:虚拟机将对象放入F-queue,创建一个低优先级的线程执行队列中对象的finalize方法finalize方法中是否重新为该对象建立引用连接
是:虚拟机将对象移出F-queue队列,对象重新回到应用阶段
否:对象可以回收
finalize方法会影响JVM的对象以及分配回收速度
finalize方法可能会让对象复活
6,终结阶段
对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段。
finalize方法代码Demo:
package com.mugua.platform;
class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive;");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK == null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK == null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
第一次成功通过finalize方法成功逃脱回收名单,但第二次因为finalize方法之前已经被调用过,因此不会在执行finalize方法,所以注定被回收了。
注意:finalizer线程执行每个对象的finalize方法时是非阻塞的,因此并不保证GC进行第二次标记时finalize方法执行完成。
7,对象内存空间重新分配阶段
GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失
四,对象模型对齐填充
64位的操作系统寻址空间2的64次方每次读取64个二进制,也就是8字节
对齐填充的意义是 提高CPU访问数据的效率,主要针对会存在该实例对象数据跨内存地址区域存储的情况.例如: 在没有对齐填充的情况下,内存地址存放情况如下:
因为处理器只能0x00-0x07,0x08-0x0F这样读取数据,所以当我们想获取这个long型的数据时,处理器必须要读两次内存,第一次(0x00-0x07),第二次(0x08-0x0F),然后将两次的结果才能获得真正的数值。
那么在有对齐填充的情况下,内存地址存放情况是这样的:
现在处理器只需要直接一次读取(0x08-0x0F)的内存地址就可以获得我们想要的数据了。
四,对象的创建过程
般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。
W我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所,直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的。
五,为什么需要两个Survivor区?
最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:刚刚新建的对象在Eden中一旦Eden满了,触发一次Minor GCEden中的存活对象就会被移动到Survivor区,这样继续循环下去下一次Eden满了的时候,问题来了,此时进行Minor GCEden和Survivor各有一些存活对象如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
六,堆内存中都是线程共享的区域吗
JVM默认为每个线程在Eden上开辟一个bufer区域,用来加速对象的分配,称之为TLAB,全称:Thread LocalAllocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。
七,方法区与元数据区以及持久代到底是什么关系
八,栈帧结构
动态链接是为了支持方法的动态调用过程。
动态链接将这些符号方法引用转换为具体的方法引用。
为了支持java的终态符号引用转变为直接引用。