JVM
线程共享的:
- 堆
- 方法区
- 直接内存(非运行时数据区的一部分)
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
本地方法栈:
虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java虚拟机栈合⼆为⼀。
堆:
从 jdk 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存
在 JDK7 前堆内存通常被分为:
- 新生代内存(Tound Generation)
- 老生代(Old Generation)
- 永生代(Permannent Generation)
在 JDK8 之后的方法区(HotSpot 的永久代)被彻底移除,取而代之的是元空间,元空间使用的是直接内存
堆内存中对象分配的基本策略:
常见策略:
- 对象优先在 eden 区分配
- 大对象直接进入老年代(大对象就是需要大量连续内存抗拒的对象,比如:字符串、数组)(为了避免为⼤对象分配内存时由于分配担保机制带来的复制⽽降低效率。)
- 长期存活的对象将进入老年代
-XX:+PrintGCDetails
如果对象在 Eden 出⽣并经过第⼀次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过⼀次 MinorGC,年龄就增加 1 岁,当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
GC 分类:
- 部分收集(Partial GC):
- 新生代收集(Minor GC / Young GC):只堆新生代进行垃圾收集
- 老年代收集(Major GC / Old GC):只堆老年代进行垃圾收集。Major GC 在有的语境中也用于指代整堆收集
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集
- 整堆收集(Full GC):收集整个 Java 堆和方法区
方法区:
⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。
JDK 1.8 的时候,⽅法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取⽽代
之是元空间,元空间使⽤的是直接内存
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最⼩⼤⼩)(如果未指定此标志,则 Metaspace 将根据运⾏时的应⽤程序需求动态地重新调整⼤⼩。)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最⼤⼤⼩(默认值为 unlimited)
运行时常量池:
运⾏时常量池是⽅法区的⼀部分。Class ⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有常量池表(⽤于存放编译期⽣成的各种字⾯量和符号引⽤)
- JDK1.7之前运⾏时常量池逻辑包含字符串常量池存放在⽅法区, 此时hotspot虚拟机对⽅法区的实现为永久代
- JDK1.7 字符串常量池被从⽅法区拿到了堆中, 这⾥没有提到运⾏时常量池,也就是说字符串常量池被单独拿到堆,运⾏时常量池剩下的东⻄还在⽅法区, 也就是hotspot中的永久代 。
- JDK1.8 hotspot移除了永久代⽤元空间(Metaspace)取⽽代之, 这时候字符串常量池还在堆, 运⾏时常量池还在⽅法区, 只不过⽅法区的实现从永久代变成了元空间(Metaspace)
Java 对象的创建过程:
- 类加载检查:
虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程
- 分配内存:
在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。
分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配⽅式由 Java 堆是否规整决定,⽽ Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定
选择以上两种⽅式中的哪⼀种,取决于 Java 堆内存是否规整。⽽ Java 堆内存是否规整,取决于GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的
指针碰撞:
- 适合场合:堆内存规整(即没有内存碎片)的情况下
- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分解之指针,只需要想着没用过的内存方向将该指针移动对象内存大小位置即可
- GC 收集器:Serial、ParNew
空闲列表:
- 适合场合:堆内存不规整的情况下
- 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块儿分给对象实例,最后更新列表记录
- GC 收集器:CMS
内存分配并发问题:
通常来讲,虚拟机采⽤两种⽅式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS配上失败重试的⽅式保证更新操作的原⼦性。
- TLAB: 为每⼀个线程预先在 Eden 区分配⼀块⼉内存,JVM 在给线程中的对象分配内存时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时,再采⽤上述的 CAS 进⾏内存分配
- 初始化零值:
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
- 设置对象头:
初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅式
- 执行 init 方法
在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始,
对象的访问定位方式:
对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:
- 句柄:如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;
- 直接指针:如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址。
这两种对象访问⽅式各有优势。使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改。使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。
如何判断对象是否死亡(即不能再被任何途径使用):
- 引用计数法
- 可达性分析算法:这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的
强引用、软引用、弱引用、虚引用:
- 弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。
- 如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被垃圾回收。
- 软引⽤可以加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣
如何判断一个类是无用的类:
同时满足 3 个条件:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地⽅被引⽤,⽆法在任何地⽅通过反射访问该类的⽅法。
垃圾收集算法:
标记清除:
该算法分为“标记”和“清除”阶段:⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。 它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
复制算法:
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。 当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。 这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。
标记整理:
根据⽼年代的特点提出的⼀种标记算法, 标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存
分代收集:
当前虚拟机的垃圾收集都采⽤分代收集算法, 这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 ⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。 ⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
垃圾回收器:
Serial 收集器:
Serial(串⾏)收集器是最基本、历史最悠久的垃圾收集器了。 简单⽽⾼效(与其他收集器的单线程相⽐)。Serial 收集器由于没有线程交互的开销,⾃然可以获得很⾼的单线程收集效率。 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
ParNew 收集器:
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全⼀样。 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
Parallel Scavenge 收集器:
Parallel Scavenge 收集器关注点是吞吐量(⾼效率的利⽤ CPU)。CMS 等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。 JDK1.8 默认使⽤的是 Parallel Scavenge + Parallel Old。使用
java -XX:+PrintCommandLineFlags -version
命令查看 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。Serial Old 收集器:
Serial 收集器的⽼年代版本,它同样是⼀个单线程收集器
Parallel Old 收集器:
Parallel Scavenge 收集器的⽼年代版本。使⽤多线程和“标记-整理”算法
CMS 收集器:
CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⾮常符合在注重⽤户体验的应⽤上使⽤。 CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。 运行过程:
- 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记:同时开启 GC 和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以 GC 线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅
- 重新标记:重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短
- 并发清除:开启⽤户线程,同时 GC 线程开始对未标记的区域做清扫。
缺点:
- 对 CPU 资源敏感
- 无法处理浮动垃圾
- 它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣
G1 收集器:
G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器. 以极⾼概率满⾜ GC 停顿时间要求的同时,还具备⾼吞吐量性能特征. 特点:
- 并行与并发:G1 能充分利⽤ CPU、多核环境下的硬件优势,使⽤多个 CPU(CPU 或者CPU 核⼼)来缩短 Stop-The-World 停顿时间。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独⽴管理整个 GC 堆,但是还是保留了分代的概念
- 空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另⼀个⼤优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为 M 毫秒的时间⽚段内。
运行步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
ZGC 收集器:
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采⽤标记-复制算法,不过 ZGC 对该算法做了重⼤改进。在 ZGC 中出现 Stop The World 的情况会更少!