天天看点

jvm虚拟器java虚拟机

java虚拟机

一 。Java内存区域

分为 与所有线程共享的 和 单独线程锁拥有的

1.1 所有线程的共享内存

  • 方法区(jdk1.8之后移除,也成永久区):

    jdk8之前,大多数人习惯称之为永久代(深入理解java虚拟机里面的东西),主要用于存储已被虚拟机加载的类信息、常量、静态变量。常量池在jdk1.7开始已经放到 堆 中了,jdk1.8 移除 永久区,增加了元空间

    元空间:

    元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存

  • 很多时候也被称做 GC 堆,虚拟机中内存区域中最大的一块,堆内存的唯一目的就是存放对象实例几乎所有的对象实例,数组 都在堆内存分配 新生代(Eden区 和 from Survivor, 和 to Survivor)+老年代

1.2 不与其他线程共享的(生命周期就是线程内)

  • 程序计数器(寄存器)

    Java虚拟机唯一没有规定任何OutOfMemoryError的区域

    是一块较小的内存空间,用来指定当前线程执行字节码的行数,每个线程计数器都是私有的,因为每个线程都需要记录执行的行数

    ;这里解释一下为什么每个线程都需要一个线程计数器?

    程序先去执行A线程,执行到一半,然后就去执行B线程,然后又跑回来接着执行A线程,那程序是怎么记住A线程已经执行到哪里了呢?这就需要程序计数器了 为了线程切换后能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器

  • 虚拟机栈 俗称 栈

    八大继承类型 和 对象引用 因为android 本身就是一个进程, 一个进程里面必有一个线程 每个方法执行的同时都会创建一个栈帧,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程 用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放的是:编译期可知的基本数据类型、对象引用类型 如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归) 拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

  • 本地(native)方法栈

    本地方法栈与java虚拟机栈作用非常类似,其区别是:java虚拟机栈是为虚拟机执行java方法服务的,而本地方法栈则为虚拟机执使用到的Native方法服务。

二 内存模型(运行时内存模型)

2.1 主内存

所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。

2.2 工作内存

每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

2,3 关系示意图

2.4 内存间操作

8中操作都是原子的,不可再分的

  1. lock(锁定),作用于主内存变量,把一个变量标记为线程独占。
  2. unlock(解锁),与lock正相反。
  3. read(读取),作用于主内存变量,它把一个变量从主内存传输到工作内存中。
  4. load(载入),作用于工作内存变量,把从read里面获取的变量放入工作内存的变量副本中
  5. use(使用),作用于工作内存变量,把变量的值传递给执行引擎。
  6. assign(复制),作用于工作内存变量,把执行引擎的值 复制给工作内存变量。同use相反
  7. store(存储),作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  8. write(写入),作用于主内存变量,把store获取的值,写入到住内存中的变量。

三 3大特性 和 指令重拍

3.1 原子性

原子是世界上的最小单位,具有不可分割性,尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用, 反应到java代码中就是---synchronized关键字,也就是说synchronized满足原子性

注意点:

比如 a=0 大致认为基本数据类型的访问读写具备原子性 long和double类型在32位操作系统中的读写操作不是原子的,因为long和double占64位, 需要分成2个步骤来处理,在读写时分别拆成2个字节进行读写 // 我们只是知道这件事情就可以 无需介意, 一般不会发生 但是市面上的jvm都规定 long 和double 是原子性操作(深入java虚拟机说的) 所以编写代码的时候 这两个不用

怎样保证?

锁、synchronized

3.2 可见性

当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改

怎样保证?
  • volatile修饰的变量,就会具有可见性 不能保证原子性 用volatile修饰的依然会有副本拷贝 是最轻量级同步的机制

    也就是说当一个线程改变一个值得时候, 会通知另一个线程 告诉他,你不要用你的工作内存(副本)中的值了, 你再重新拿主内存的值吧

  • final也会可见性(暂时了解)
  • synchronized能够实现:1.原子性(同步) 2.可见性

3.3 顺序性

即jvm程序执行的顺序按照代码的先后顺序执行

3.4 指令重拍

JVM可以对它们在 "不改变数据依赖关系" 的情况下进行任意排序以提高程序性能。 一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的虽然重排序不会影响单个线程内程序执行的结果,但是会影响到多线程

例子1

线程1
int a = 1
a++;

线程2
if(a>2){
    ...
}
复制代码
           

线程1 肯定会按照写的顺序执行的, 但是当执行2的时候,有可能对1重新排序。 因为 2依赖于1中的a, 所以会有不确定值

例子2: 单利模式

synchronized存在巨大的性能开销 所以用了双重校验

public class Singleton {
	private Singleton() { }
	private volatile  Singleton instance;
	public Singleton getInstance(){ 
		if(instance==null){// 1:第一次检查
			synchronized (Singleton.class){ // 2:加锁
				if(instance==null){ // 3:第二次检查
					instance = new Singleton();// 4:加 volatile关键字 的根源出在这里
				}
			}
		}
		return instance;
	}
}
}
复制代码
           

这里为什么要加volatile了?

在jdk1.5之前不能完全保证指令重拍的,之后才有了双重校验锁机制.

问题 出现在 instance = new Singleton() 因为这条语句有下面的操作

  1. 分配对象的内存空间
  2. 初始化对象;
  3. 设置instance指向刚分配的内存地址

当a线程正在初始化这个单利的时候 ,此时另一个b线程执行到 步骤1的时候 发现instance !=null , 此时 jvm 有可能对 a线程指令重拍 假如 2 和 3 重拍的话 b线程 拿到就是不对的

四 gc

4.1 怎样判断对象是否已死?

1.引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用时效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,引用计数算法管理内存很高效, 但是java虚拟机没有使用,因为是他很难解决对象间相互循环引用的问题。两个对象互相引用,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

2.引用计数算法

这个算法的基本思路就是通过一系列的称为“GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。 这个算法防止 AB互相引入

4.2 可以作为gc roots的

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中静态类属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

4.3 Java中常用的垃圾收集算法

1.引用计数算法 老年代 永久代

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收

2.复制算法: 年轻代

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

2.1 缺点:

  • (1)效率问题:在对象存活率较高时,复制操作次数多,效率降低;
  • (2)空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)
3. 标记-整理算法: 老年代 永久代

先标记所有该回收的,然后整理到而是将存活的对象都向一端移动,最后直接清理掉边界以外的内存

4. 分代收集算法

频繁收集生命周期短的区域(Young area);

比较少的收集生命周期比较长的区域(Old area);

基本不收集的永久区(Perm area)。

  • 新生代:朝生夕灭的对象(例如:方法的局部变量等)。 gc 15 次还不死的话 进入老年代 只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成 gc完成之后,复制没有被回收的对象 细分: Eden区 和 from Survivor, 和 to Survivor, 比例 8:1:1 除法gc的时候 把 eden和from区 没有被回收的对象放到 to区 , eden和from 全部回收 之后 把这次的to变成from *老年代:有的大对象直接进入老年代 (很长的字符串 和 数组) ,存活得比较久,但还是要死的对象(例如:缓存对象、单例对象等)。
  • 永久代:对象生成后几乎不灭的对象(例如:加载过的类信息)。
  • 所以老年代和永久代 用 标记-清除算法 或者 标记-整理算法