Java语言的一个非常重要的特点就是与平台的无关性,它跨平台移植性的优点则是通过JVM实现的。

让我们来看下Java程序的运行流程:首先,Java代码被编译成字节码文件,再由JVM将字节码文件翻译成机器语言,从而达到运行Java程序的目的。不同平台下编译生成的字节码是相同的,但是由不同平台下JVM不同(比如Windows版的JVM、UNIX版的JVM),所以翻译成的机器码也不同。

那么JVM(Java Virtual Machine)的原理和运作机制是什么?我们一起来看。

  • 1.运行时数据区域:JVM内存结构
  • 2.垃圾收集机制:
  • 3.内存分配与回收策略:
  • 4.类加载机制:
  • 5.虚拟机调优:
  • 6.JVM与并发编程:Java内存模型

1. 运行时数据区域

运行时数据区域对应JVM内存结构

1.1. 五大主要区域

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

程序计数器是当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节码指令的地址。

线程权限:线程私有。 每条线程都需要一个独立的线程计数器,来保证线程切换后能恢复到正确执行位置。

执行本地(Native)方法时,计数器值为空。

1.1.2. Java虚拟机栈(JVM Stack)

Java虚拟机栈为虚拟机执行Java方法,生命周期与线程相同。每个方法被执行时,JVM都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法出口等信息。局部变量表存放了基本数据类型、对象引用类型、returnAddress类型

线程权限:线程私有

可能抛出异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常。
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

1.1.3. 本地方法栈(Native Method Stacks)

本地方法栈类似Java虚拟机栈,为本地方法服务。本地方法一般是由其他语言(非Java语言,如C、C++或汇编等)编写。

线程权限:线程私有

可能抛出异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

1.1.4. Java堆(Java heap)

Java堆的作用是用于存放对象及数据实例。Java堆是垃圾收集器管理的内存区域,因此也叫“GC堆”。现代的垃圾收集器基本都采用分代收集算法,其主要思想是针对不同类型的对象采取不同的垃圾回收算法,详细请参阅 垃圾回收机制

堆大致由两部分组成:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

线程权限:线程共享

可能抛出异常:

  • 堆并不要求内存空间一定是连续的,堆的大小可动态扩展,增加失败时会派出 OutOfMemoryError 异常。

可通过JVM参数 -Xms(初始大小)-Xmx(最大值) 来指定一个程序的堆内存大小。

java -Xms256M -Xmx512M JavaApplication

1.1.5. 方法区(Method Area)

方法区存放已被JVM加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

线程权限:线程共享

可能抛出异常:

  • 和堆一样不需要连续的内存,方法区大小可以动态扩展,扩展失败一样会抛出 OutOfMemoryError 异常。

HotSpot虚拟机曾把它当成永久代来进行垃圾回收。从JDK1.8开始,移除永久代,并把方法区移到元空间中。

1.2. 两个补充区域

1.2.1. 运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量与符号引用。在类被加载后,会放到方法区的运行时常量池中。

不仅在编译期生成常量,还允许运行期间动态生成常量放入池中,比如String类的intern()方法

可能抛出异常:

  • 因为属于方法区的一部分,当常量池无法申请到内存时,会抛出 OutOfMemoryError 异常。

1.2.2. 直接内存(Direct Memory)

直接内存,又称堆外内存,属于操作系统级的内存,它不是JVM运行时区域的一部分,也不是Java虚拟机规范中定义的内存区域。

代码中可以使用ByteBuffer.allocateDirect()申请直接内存。

可以通过 -XX:MaxDirectMemorySize 参数来设置最大可用直接内存,如果启动时未设置则默认为最大堆内存大小,即与 -Xmx 相同。即假如最大堆内存为1G,则默认直接内存也为1G,那么 JVM 最大需要的内存大小为2G多一些。当直接内存达到最大限制时就会触发GC,如果回收失败则会引起OutOfMemoryError。

为什么需要直接内存:

  1. 在性能上,申请直接内存不如申请堆内存快。
  2. 但是在进行本地IO操作时 对于频繁的IO操作,我们需要不断把内存中的对象复制到直接内存。然后由操作系统直接写入磁盘或者读出磁盘。避免在堆内存和堆外内存来回拷贝数据。

2. 垃圾收集机制

2.1. 哪些内存需要回收(WHAT)

如何判断出不能再被使用的对象?

2.1.1. 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;计数器为零的对象就是需要被回收的。

引用计数算法算法有一个问题,两个对象出现循环引用时,它们的计数器值不为0,无法进行回收

循环引用代码

a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象各自本身的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收

2.1.2. 可达性分析算法

从起始节点集GC Roots开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连,也就是说从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

哪些对象可作为GC Roots的对象?

固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(StringTable)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

2.1.3. 引用类型

强引用(Strongly Re-ference)、软引用(SoftReference)、弱引用(Weak Reference)和虚引用(Phantom Reference),这4种引用强度依次逐渐减弱

强引用(Strongly Re-ference) 软引用(SoftReference) 弱引用(Weak Reference) 虚引用(Phantom Reference)

2.2. 什么时候回收(WHEN)

2.3. 垃圾收集算法(HOW)

2.3.1. 标记-清除

2.3.2. 标记-复制

2.3.3. 标记-整理

2.3.4. 分代收集

2.4. 垃圾收集器

2.4.1. Serial收集器

2.4.2. ParNew收集器

2.4.3. Parallel Scavenge收集器

2.4.4. Serial Old收集器

2.4.5. Parallel Old收集器

2.4.6. CMS收集器

2.4.7. G1(Garbage First)收集器

3. 内存分配与回收策略

3.1. Minor GC 和 Full GC

3.2. 内存分配

3.2.1. 对象优先在Eden分配

3.2.2. 大对象直接进入老年代

3.2.3. 长期存活的对象将进入老年代

3.2.4. 动态对象年龄判定

4. 类加载机制

4.1. 双亲委派模型

5. 虚拟机调优

5.1. 性能调优工具

6. JVM与并发编程

6.1. Java内存模型

7. 参考资料

results matching ""

    No results matching ""