一、JDK内存结构
1、程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。线程私有,即每个线程都有一个独立的程序计数器。
其主要作用是:在多线程环境下,当线程切换回来时,能够知道该线程上次执行到哪里,从而继续执行。
这个区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2、虚拟机栈
虚拟机栈描述的是 Java 方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。线程私有,生命周期与线程相同。
- 局部变量表:存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型(指向了一条字节码指令的地址)。
- 操作数栈:用于存储方法执行过程中的中间结果。
- 动态链接:指向运行时常量池中该栈帧所属方法的引用。
- 方法出口:记录方法执行完毕后返回的地址。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 *Error 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
3、本地方法栈
本地方法栈与虚拟机栈的作用类似,只不过虚拟机栈为执行 Java 方法服务,而本地方法栈则为执行 Native 方法服务。线程私有。
在 HotSpot 虚拟机中和虚拟机栈合二为一。同样可能抛出 *Error 和 OutOfMemoryError 异常。
4、堆
堆是 JVM 内存中最大的一块区域,用于存放对象实例,几乎所有的对象实例都在这里分配内存。线程共享,是垃圾回收的主要区域。
在 JDK1.8 中,堆分为新生代和老年代。
-
新生代:
- Eden 区:新创建的对象首先会在 Eden 区分配内存。
- Survivor From 区和 Survivor To 区:当 Eden 区满了之后,会触发 Minor GC(新生代垃圾回收),将存活的对象复制到 Survivor From 区,经过多次 Minor GC 后,仍然存活的对象会被移动到老年代。Survivor From 区和 Survivor To 区在每次 Minor GC 后会互换角色。
-
老年代:存放经过多次 Minor GC 后仍然存活的对象。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
5、方法区
在 JDK1.8 中,方法区的实现变为元空间,使用本地内存,不再受限于 JVM 内存大小,只受限于本地内存大小。
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。线程共享。
如果方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
二、GC(垃圾回收)
-
垃圾判定
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。但是这种方法无法解决对象之间相互循环引用的问题。
- 可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
-
垃圾回收算法
- 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点是会产生大量不连续的内存碎片,可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
- 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法实现简单,运行高效,但缺点是将内存缩小为了原来的一半。
- 标记-整理算法:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
JDK1.8 中的垃圾回收器
- 新生代回收器:
- Serial 收集器:单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
- ParNew 收集器:是 Serial 收集器的多线程版本。
- Parallel Scavenge 收集器:关注的是吞吐量,即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
- 老年代回收器:
- Serial Old 收集器:Serial 收集器的老年代版本,单线程,采用“标记-整理”算法。
- Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,多线程,采用“标记-整理”算法。
- CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间为目标的收集器,采用“标记-清除”算法,整个过程分为四个阶段:初始标记、并发标记、重新标记、并发清除。
- G1(Garbage-First)收集器:面向服务端应用的垃圾收集器,将整个 Java 堆划分为多个大小相等的独立区域(Region),跟踪各个 Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
- 新生代回收器:
三、类加载
-
类加载过程
- 加载:通过类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
- 验证:确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。
- 解析:将常量池内的符号引用替换为直接引用的过程。
- 初始化:执行类构造器 ()方法的过程,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
-
类加载器
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且被虚拟机识别的类库。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载用户类路径(ClassPath)上所指定的类库。
-
双亲委派模型
- 当一个类加载器收到类加载的请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 好处是可以确保 Java 核心库的类型安全,防止用户自己编写的类冒充核心类库中的类。