JVM常见知识点
jvm
Java内存区域与内存溢出异常
程序计数器(Program Counter Register, PCR)
当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。每条线程都需要有一个独立的PCR,各条线程之间PCR互不影响,独立存储。此内存区域是唯一一个在Java虚拟机规范里没有规定任何OOM情况的区域。
如果线程执行的是一个Java方法:PCR记录的是正在执行的虚拟机字节码指令的地址。
如果线程执行的是一个Native方法:PCR则为空(Undefined)。
Java虚拟机栈(Java Virtual Machine Stacks,JVMS)
此部分也是线程私有的,它的生命周期与线程相同。用于存储局部变量表、操作数栈、动态链接、方法出口等信息。Java虚拟机规范对这个区域定义了两种异常状况:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存。
本地方法栈(Native Method Stack,NMS)
与JVMS功能相同,不过是为虚拟机的Native方法服务,而JVMS是为虚拟机的Java方法服务。
Java堆(GC堆)
被所有线程共享的一块内存区域,用来存放对象实例。Java堆中可以细分为:新生代、老年代。
当堆里没有内存,且无法再扩展时,抛出OOM异常。
方法区(Method Area,MA)(Non-Heap非堆)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这个区域的内存回收目标主要是对常量池的回收和对类型的卸载。
此区域还包括运行时常量池(Runtime Constant Pool)。
直接内存(Direct Memory)
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
NIO类里的I/O方式,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据。
OOM异常
Java堆溢出:对象数量到达最大堆的容量限制后就会产生内存溢出异常。
虚拟机栈和本地方法栈溢出:见上 ↑。
方法区和运行时常量池溢出:内存不够了呗。
本机直接内存溢出:内存不够了呗。
垃圾收集器与内存分配策略
对象死了吗
判断方法:
- 引用计数算法:给对象中添加一个引用计数器,它很难解决对象之间循环引用的问题。
- 可达性分析算法:一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
Java里的引用
- 强应用(Strong Refreence):new等,垃圾回收期不会回收掉有强引用的对象。
- 软引用(Soft Reference):还有用但并非必需的对象,在OOM异常抛出之前,会回收。
- 弱引用(Weak Reference):被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
- 虚引用(Phantom Reference):唯一目的是能在这个对象被收集器回收时收到一个系统通知。
垃圾收集算法
- 标记-清除算法(Mark-Sweep):标记,然后清除。
- 复制算法(Copying):将还存活的对象赋值到另一块内存上面,然后一次性清理掉该区域。
- 标记-整理算法(Mark-Comopact):标记,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法(Generational Collection):一般是将Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。新生代–>复制算法;老年代–>标记-清理,或者标记-整理算法。
内存分配与回收策略
- 对象优先在新生代Eden区(伊甸区)中分配。
- 大对象直接进入老年代。
- 长期存活的对象将进入老年代。
- 动态对象年龄判定,是否进入老年代。
虚拟机类加载机制
在这张图里,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的。解析阶段则不一定:它在某些情况下可以在初始化之后再开始。
有且只有5种情况必须立即对类进行“初始化”。
- 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、调用一个类的静态方法。
- 使用java.lang.reflect包里的方法对类进行反射调用的时候,若类未初始化,则初始化。
- 初始化一个类时,若其父类未初始化,则先出发其父类的初始化。
- 虚拟机启动时,用户需指定一个主类进行初始化。
- 使用JDK7的动态语言支持时,如果……
类加载的过程
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配,但要注意:
- 这里指的变量仅包括类变量(被static修饰的变量),而不包括实例变量。
- 这里指的初始值“通常情况”下是数据类型的零值。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
高效并发
Java内存模型与线程
主内存和工作内存
主内存存储了所有的变量。
工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
住内存主要对应Java堆中的对象实例数据部分,而工作内存则对英语虚拟机栈中的部分区域。
内存间交互操作
Java内存模型中定义了以下8种操作来完成:
- lock
- unlock
- read
- load
- use
- store
- assign
- write
对于volatile型变量的特殊规则
当一个变量定义为volatile之后,它将具备两种特性:
- 保证此变量对所有线程的可见性。可见性是指一旦工作内存中此变量的值发生了改变,将会立即同步到主内存,当主内存里的此变量被使用之前,都会刷新主内存。
- 禁止指令重排序优化。此语义可以被用来避免某行代码被提前执行。
线程的实现
- 使用内核线程实现
- 使用用户线程实现
- 使用用户线程加轻量级进程混合实现
Java线程调度
- 协同式线程调度
- 抢占式线程调度
Java语言中,如果一个变量要被多线程访问,可以使用volatile关键字声明它为“易变的”;如果一个变量要被某个线程独享,可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。