一、java内存区域
- 程序计数器:线程私有,唯一一个不会出现outOfMemoryError的内存区域
- 虚拟机栈:线程私有,栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
- 本地方法栈:线程私有,为虚拟机栈使用到的Native方法服务
- 堆:线程共享,内存最大的一块,唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存 。
- 方法区(1.8之后叫元空间):线程共享,逻辑区域,存储已被虚拟机加载的:类信息、字段信息、方法信息、常量、静态变量、即时编译期编译后的代码缓存等数据
- 直接内存:线程共享,特殊的内存缓冲区,是通过JNI的方式在本地内存上分配的 。
【重】为什么将永久代替换为元空间?
1.整个永久代有一个JVM本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存限制,溢出几率比原来更小。
2.元空间里面存放的是类的元数据,这样加载多少类的元数据不由MaxPermSize控制了,而由系统的实际可用空间来控制,这样能加载更多的类。
3.在jdk8中,合并HotPot和JRockit的代码时,JRockit从来没有一个叫永久代的东西,因此合并后无序再增加。
【重】jdk1.7为什么将字符串常量池移动到堆中?
因为永久代(方法区的实现)的GC回收率太低了,只有在整堆收集(Full GC)的时候才会被执行GC。java程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够高效及时地回收字符串内存。
二、垃圾收集算法
-
**标记-清除(**Mark-and-Sweep)首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。【效率低,且会产生大量不连续的内存碎片】
-
复制算法:它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。【可用内存变小、不适合老年代】
-
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。【效率不高,适合老年代】
-
分代收集算法 :当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法【新生代采用标记-复制算法,老年代采用标记-整理算法】
三、垃圾收集器
-
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。它的 "单线程" 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。【效率高、体验感不好】
-
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
-
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。关注点是吞吐量(高效率的利用 CPU)
-
Serial Old收集器:Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
-
Parallel Old收集器:Parallel Scavenge 收集器的老年代版本。使用多线程和"标记-整理"算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
-
**CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。**CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。【对CPU资源敏感、无法处理浮动垃圾、会产生大量空间碎片】
-
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器 。运作过程:初始标记、并发标记、最终标记、筛选回收。
【重】 虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生
【重】对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是"非死不可"的,这时候它们暂时处于"缓刑阶段",要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize
方法。当对象没有覆盖 finalize
方法,或 finalize
方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收
四、内存分配策略
部分收集 (Partial GC):
-
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
-
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
-
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区
五、类加载的过程
它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
六、类加载器
-
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
-
每个 Java 类都有一个引用指向加载它的
ClassLoader
。 -
数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
【类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)
.双亲委派【重】
-
ClassLoader
类使用委托模型来搜索类和资源。 -
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
-
ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
简单总结一下双亲委派模型的执行流程:
-
在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
-
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 -
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 -
如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常。