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。
为什么需要直接内存:
- 在性能上,申请直接内存不如申请堆内存快。
- 但是在进行本地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)