Files
knowledge-base/AI/mechine learning/深入理解Java虚拟机.md
T
2026-08-25 22:28:36 +08:00

19 KiB
Raw Blame History

深入理解Java虚拟机

来源:原 mongodb 集合 t_blog · 整理日期:2026-08-25 分类 ID:00000000000003 · 排序:0 · 文章数:9

目录

  1. 谈一下垃圾回收算法
  2. Jvm
  3. 2.1运行时数据区域
  4. 2.2对象内存
  5. 13.1线程安全与锁优化
  6. 12.1Java内存模型和线程
  7. 12.2关键字volatile
  8. 12.3 原子性,可见性,有序性
  9. 12.4 Happens-Before

1. 谈一下垃圾回收算法

bid: 00000000000001

Garbage Collection アルゴリズム


参考文章

  对于Java开发人员来说,失去了引用的对象通常会被GC回收,而我们的方法都是我们的虚拟机栈中执行的,方法执行完毕后,引用被销毁,剩余的对象自然而然就是会被GC回收。 就如参考文章所说,JVM种虚拟机栈是随线程开启而创建,线程销毁而结束。   由于新的垃圾回收器G1的的出现,和方法区(永久代)的废弃,取而代之的metaspace的出现,以往的游戏知识已过时。但是其中的基本算法思想还是学要学习的。

基本算法思想:

标记-清除(mark-sweep)


标记是一个过程,他是通过可达性分析算法来实现的。 这里标记的通常是不被清除的对象,因为这样的对象很少。因为大多数对象的都是一次性的。 清除也是一个过程,它是基于分代收集理论实现的。清除未被标记的对象。 这种算法很明显会产生内存碎片化,内存不连续会直接导致提前gc的发生。

为了避免内存的碎片化,就有了复制算法。

复制算法


复制算法是建立在标记清除算法的基础上的(即也存在标记和清除的过程)。 标记存活对象,复制对象到存活区,清除其他区域所有对象。 复制算法,既然是复制,就要保证左右两边的容量相等,否则会出现数组越界等情况。 这种算法典型用在年轻代的minor gc上。 基于这种算法,jvm内存 配分成eden,s0和s1,8:1:1的比例。 其中s0和s1的0和1没有任何区别。这样的设计比例较好的保证了大部分对象的产生和一小部分的对象进入存活区。虽然牺牲了1的部分的空间。 new 出来的对象都会在eden中,当eden满了后,触发minor gc, 将存活对象移动到s0,清除eden,当eden第二次满了之后,将存活对象复制到s1,清除eden和s0,以后反复如此。

a


2. Jvm

bid: 00000000000004

Jvm


Java Virtual Machine(Jvm)Java虚拟机 是跨平台的关键因素。 对于Jvm需要了解的有两大模块,一是内存模型,二是垃圾回收机制。

内存模型


随着Java的版本的变化,内存模型也有一定的变化。 比如Java8后,Metaspace代替了permanent space。 但是内存模型这个抽象概念没有消失,java虚拟机内存通常被划分为以下几个部分。 heap: stack: native method stack 程序计数器

垃圾回收机制/Garbage collections


垃圾回收机制包括算法部分和堆内存分配两部分。 heap内存:按照java的对象的存活时间分为以下几个部分。 Young Generation: Old Generation: 由于Meta Space是使用本地内存,他不算到heap里面。 Eden space:所有的


3. 2.1运行时数据区域

bid: 2022050519382600000000000012

1.程序计数器(Program Counter Register)


线程独有:用于记录线程的指令位置(内存地址)。  如果线程正在执行的是一个java方法,那么程序计数器记录的正在执行的虚拟机字节码指令的地址;如果,正在执行的是本地Native方法,这个计数器的值应该为空(Undefined)。 此内存区域是《Java虚拟机规范》中唯一没有规定OutofMemoryError的区域。

2.虚拟机栈(Java Virtual Machine Stack)


线程独有:是Java方法执行的内存模型。  每一个方法被执行的时候,Jvm会为该方法在该线程中创建一个栈帧(Stack Frame),用于存储 局部变量表,操作数栈,动态链接,方法出口等信息。  一个方法被调用,即为入栈(压栈),一个方法调用结束,即为出栈(弹栈)。不难发现栈是一个先进后出的QUEUE。 局部变量表中存放了编译器可知的基本类型,对象引用,returnAddress类型(指向一条字节码指令的地址)。  这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)表示。其中64位的long和double需要两个槽。其余的数据类型只占一个Slot,局部变量表的所需的内存空间在编译期间完成分配,当进入一个方法后,方法需要在栈帧中分配的变量空间是完全确定的。这个“大小”是指的槽的数量。真正一个槽多大空间是虚拟机决定的。  虚拟机栈中有两个异常:一个是,StackOverFlowError,栈深度异常。另一个是OutofMemoryError,是可选的(当如果虚拟机栈可以动态扩展,但又申请不到足够内存时,发生,HotSpot虚拟机不允许,动态扩展)。

3.本地方法栈


线程独有:是Java native方法执行的空间区域。  本地方法栈和虚拟机的作用是极其相识的,只是本地方法栈只为native method服务。

4.Java 堆


线程共享:是Java对象和数组的存放的主要区域。  heap,随着java的技术的进步,所有的对象和数组都分配在堆中的z,已经不是那么绝对了。 但是java垃圾回收器管理的区域就是堆。 由于垃圾回收算法是基于分代类论的,但是随着java的垃圾回收技术的提升,堆的分配已经复杂化了,至少在G1垃圾回收器目前来看。

5.方法区


《Java虚拟机规范中的》的Non-heap。 线程共享:存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓冲的数据。 其中运行时常量池作

6.直接内存


 既不是,运行时数据的一部分,也不是《虚拟机规范》中定义的区域,但这部分内存也会被频繁使用,而且可能导致OutOfMemoryError。 JDK1.4加入了NIO类,引入了一种基于通道(channel)和缓冲区(buffer)的I/O模型,它可以使用native 方法直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象,作为这块内存的引用来进行操作,从而避免了Java堆和Native堆中的来回复制。


4. 2.2对象内存

bid: 2022050620338300000000000015

在HotSpot虚拟机里,对象在堆中的存储布局可以划分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)

对象头(Header)


对象头部包括两类信息,一类是用来存储对象自身的运行时数据,包括HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,这里的偏向是指 offset,毕竟引用所指向的对象位置不是一个区域,而是一个点。 这部分数据长度在32/64中的长度为,4个和8个字节,被称为Mark Word。 比如在32位下,对象未被同步锁的情况下。

存储内容 bit state
HashCode 25
分代年龄 4
存储锁标志位 2
固定为0 1

5. 13.1线程安全与锁优化

bid: 2022050710179900000000000027

synchronized

既可以用作对象锁也可以用做类锁

类锁和对象锁

synchronized 对象锁: 每个对象都有一个monitor,而一个monitor只能被一个线程所拥有,当一个线程a进入monitorenter,monitor的进入数为1,同时,其他的线程就无法获取monitor,如果a再次进入monitorenter,monitor的进入数为2,所以synchronized 是可重入锁。当a monitorexit退出对象锁,则monitor的进入数减少为1,只有monitor的进入数为0时候,即没有任何线程拥有monitor

对象锁
synchronizded method  // 直接获取该方法的对象的对象锁
synchronizded(object) { // 获取object的对象锁
}

类锁
static synchronizded method
synchronizded(xxx.class) {
}

-- 类锁和对象锁不会产生竞争,二者的加锁方法不会相互影响

锁在哪? biased lock 偏向锁

我们知道java对象有三部分组成
对象头部 object header
实例数据 object data
对齐填充 向2的*n填充

而对象头部由Mark Word和klass pointer组成
前8个字节为Mark Word,后4个字节为klass pointer
其中Mark Word长度为jvm一个word的大小。64位jvm是64bit,即8个字节。
-------------------------------------------------
OFFSET  SIZE  TYPE                 DESCRIPTION
0       4     (object header)      markword
0       4     (object header)      markword
8       4     (object header)      klass pointer
-------------------------------------------------
[open jdk document](http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp)

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

unused    hash    unused    age    biased_lock    lock
25     -> 31   -> 1      -> 4   -> 1           -> 2    normal object(sum = 64)


无锁状态下的mark word
Java 对象刚创建时,还没有任何线程来竞争
unused    hash    unused    age    biased_lock    lock
25     -> 31   -> 1      -> 4   -> 0           -> 01

偏向状态下的mark word
偏向锁是指一段同步代码一直被一同个线程所访问,那么该线程会自动获取锁,降低获取锁的代价
偏向锁在竞争不激烈的情况下,效率非常高。
线程ID    epoch   unused    age    biased_lock    lock
54     -> 2   -> 1       -> 4   -> 1           -> 01

轻量级锁状态下的mark word
当有两个线程开始竞争这个锁对象,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,Mark Word 就指向哪个线程的栈帧中的锁记录
锁记录指针                                         lock
62                                              -> 00

重量级锁状态的mark word
重量级锁会让其他申请的线程之间进入阻塞,性能降低。
重量级锁也就叫做同步锁,这个锁 对象 Mark Word 再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形 式,来登记和管理排队的线程。
monitor对象指针                                    lock
62                                              -> 10

synchronized 的锁优化

JDK1.6开始的减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
需要注意的是锁可以升级但不能降级

关于 Object 和 Thread

Object 类 有native 的 wait方法
调用wait 前, obtain monitor,
调用wait 后,释放对象锁,然后当前线程进入等待状态wait set 队列中

sleep() 不释放锁,sleep 可以在没有锁的情况下调用,

6. 12.1Java内存模型和线程

bid: 2022050920343900000000000005

概述


衡量一个服务性能好坏的,每秒事务处理数(Transactions Per second,TPS ),是重要指标之一。

硬件的效率和一致性


 由于计算机的存储效率和计算效率的相差甚大,计算机不得不引入读写速度接近于处理器运算速度的高速缓存(cache),来作为内存和cpu直接的缓冲,但也增加了计算机系统的复杂性,尤其在多核处理器时代,每个处理器都有自己的缓存,而他们又共享同一块内存,这样就导致了一个新的问题:内存一致性。  为了解决内存一致性,各个处理器访问缓存时都遵循一些协议,比如MSI,MESI等,而java虚拟机同样有自己的内存模型,并且和上述介绍的硬件和缓存访问操作具有高度的类比性。  除了增加高速缓存之外,为了使处理器的运算单元尽量的被充分利用,处理器可能对输入的代码进行乱序执行优化,,处理器会在计算后将乱序执行的结果重组,保证最终结果一致性。 而与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有指令重排序(Instruction Recorder)优化。

Java 内存模型


主内存和工作内存。 首先要明确一点,很容易混淆的是,虚拟机栈是一个jvm运行时的存储结构,并不是运算。 从更基础的层次来说,主内存直接对应物理硬件的内存,工作内存对应于寄存器和高速缓存。 而工作内存直接存储了被该线程使用的变量的主内存副本,这里的副本,有可能是对象的引用或者,对象的某个字段,但不会有虚拟机把整个java对象复制一次的。

内存间的交互操作


Java内存模型规定了一些操作来实现内存间交互,JVM会保证它们是原子的 lock: 锁定,把变量标识为线程独占,作用于主内存变量 unlock: 解锁,把锁定的变量释放,别的线程才能使用,作用于主内存变量 read: 读取,把变量从主内存读取到工作内存 load: 载入,把read读取到的值放入工作内存的变量副本中 use: 使用,把工作内存中的一个变量的值传递给执行引擎 assign: 复制,把从执行引擎接收到的值赋给工作内存里面的变量 store: 存储,把工作内存中一个变量的值传递到主内存中 write: 写入,把store进来的数据存放到主内存的变量中

注意,Java内存模型,只要求上述两个操作必须按顺序执行,但没有要求连续执行。也就是说read和load之间store与write之间是可以插入其他的指令的。比如对主内存中a,b变量进行访问时,可能出现,read a,read b,load b,load a。 后来java设计团队,对java内存模型进行了简化,read,write,lock,unlock四种


7. 12.2关键字volatile

bid: 2022050921201400000000000006

关键字volatile是java虚拟机提供的最轻量级的同步机制。 当一个变量被定义为volatile时,它具备两种特性。 第一种是可见性。 即每次get时,强制刷新。 一个误解区,字节码就是原子操作,这是不对的,一个字节码指令在解释执行时,如果是编译执行,一条字节码会有可能被转化为若干个本地机器指令。此处,用-XX:+PrintAssembly参数输出反汇编来分析。

第二种是,有序性。 即,禁止指令重排序(Instruction Recorder)优化。 被volatile修饰的变量,赋值后会多执行一个"lock addl"操作,这个操作相当于一个memory barrier(内存屏障),指令重拍时,不能把后面的指令排序到内存屏障前面去,只有一个处理器访问时,不需要内存屏障。 addl 是一个空操作,(把ESP寄存器的值 + 0),这个操作相当于把缓存中的变量做了一次store和write操作。 lock iddl指令把修改同步到内存时,意味着所有的之前的操作都已完成,便形成了指令重排序无法穿越内存屏障的效果。 针对于long和double的类型变量,不通虚拟机是有着不同的实现,此处不做描述。


8. 12.3 原子性,可见性,有序性

bid: 2022050921544800000000000007

Java内存模型,是围绕着在并发过程中如何处理原子性,可见性,和有序性的这三个特点建立的。

原子性(Atomicity)


由Java内存模型直接保证的原子性操作有,read,load,assign,use,store,write这6个,我们大致可以认为,对基本类型的访问读写都是原子性的(long double类外)。 尽管,虚拟机没有把lock和unlock操作直接开放给用户,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地这两个操作,反映到同步块就是synchronized关键字,因此synchronzed块之间的操作也属于原子操作。

可见性 (Visibility)


可见性,即变量的修改对其他线程是可见的,无论是普通变量还是volatile变量都是会将结果刷新到主内存的,只是volatile是其他线程获取该变量时强制刷新到主内存,而普通变量并没有。 除此之外,java还有两个关键字能够实现可见性,即synchronized和final synchronized的可见性是因为"对一个变量unlock操作之前,必须先把此变量同步回到主内存中(即执行了store,write操作)"的这条规则获得的。 而final的可见性是指,被final修饰的字段在构造器中一旦完成了初始化,并且构造器并没有把这个引用传递出去,那么其他线程就能看见final字段。

有序性 (Ordering)


Java语言提供了volatile和synchronized两个关键字来保证线程之间的操作是有序性的。 volatile本身就是禁止了指令重排序。 而synchronized是由"一个变量在同一时刻只允许有一条线程对其进行lock操作"

synchronzed能保证以上三个特性,但是并发场景会影响性能。


9. 12.4 Happens-Before

bid: 2022050922310000000000000008

先行发生原则(Happens-Before)

Java内存模型中如果所有的有序性都仅靠volatile和synchronized来完成的话,会有很多操作变得啰嗦。 Java语言有一个先行发生原则(Happens-Before)。 规则: 1.程序次序规则 2.Monitor Lock Rule 3.volatile Variable Rule 4.Thread Start Rule,start方法最先执行 5.Thread Termination Rule,所有方法都优先于 6.线程中断规则。 7.对象终结规则,一个对象初始化完成先行发生于finalize()方法。 8.传递性,A》B,B》C,那么A》C jintian 1111