1. 垃圾回收机制
Stop-the-World:
JVM由于要执行GC而停止了应用程序的执行称之为Stop-the-World,该情形会在任何一种GC算法中发生。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。
2. java运行时的内存划分
1. 程序计数器
记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。
当多线程运行时,每个线程切换后需要知道上一次所运行的状态、位置。
由此也可以看出程序计数器是每个线程私有的。
3. 虚拟机栈
虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。
每一个栈帧由局部变量区、操作数栈等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。
- 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 StackOverflowError。
- 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 StackOverflowError。
- 若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出 OutOfMemoryError。
这块内存区域也是线程私有的。
4. Java堆
Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。
可利用参数 -Xms -Xmx 进行堆内存控制。
这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用分代回收算法,所有堆内存也分为 新生代、老年代,可以方便垃圾的准确回收。
这块内存属于线程共享区域。
5. 方法区
方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量。 这块区域也被称为永久代。
可利用参数 -XX:PermSize -XX:MaxPermSize 控制初始化方法区和最大方法区大小。
6. 元数据区
在 JDK1.8 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(Metaspace)。
默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 java.lang.OutOfMemoryError: PermGen。
但也不能无线扩展,因此可以使用 -XX:MaxMetaspaceSize来控制最大内存。
7. 运行时常量池
运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用。
8. 直接内存
直接内存又称为 Direct Memory(堆外内存),它并不是由 JVM 虚拟机所管理的一块内存区域。
有使用过 Netty 的朋友应该对这块并内存不陌生,在 Netty 中所有的 IO(nio) 操作都会通过 Native 函数直接分配堆外内存。
它是通过在堆内存中的 DirectByteBuffer 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为零拷贝。
既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 JVM 管理,所以常规 GC 操作并不能回收堆外内存。它是借助于老年代产生的 fullGC 顺便进行回收。同时也可以显式调用 System.gc() 方法进行回收(前提是没有使用 -XX:+DisableExplicitGC 参数来禁止该方法)。
值得注意的是:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。
9. 常用参数
通过上图可以直观的查看各个区域的参数设置。
常见的如下:
-
-Xms64m 最小堆内存 64m.
-
-Xmx128m 最大堆内存 128m.
-
-XX:NewSize=30m 新生代初始化大小为30m.
-
-XX:MaxNewSize=40m 新生代最大大小为40m.
-
-Xss=256k 线程栈大小。
-
-XX:+PrintHeapAtGC 当发生 GC 时打印内存布局。
-
-XX:+HeapDumpOnOutOfMemoryError 发送内存溢出时 dump 内存。
新生代和老年代的默认比例为 1:2,也就是说新生代占用 1/3的堆内存,而老年代占用 2/3 的堆内存。
可以通过参数 -XX:NewRatio=2 来设置老年代/新生代的比例。
对象的创建
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,需要掌握)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
内存分配并发问题(补充内容,需要掌握)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
-
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
-
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
最后
由于篇幅限制,小编在此截出几张知识讲解的图解,有需要的程序猿(媛)可以点赞后戳这里免费领取全部资料获取哦
]
[外链图片转存中…(img-NwBVpf9x-1624947332610)]
[外链图片转存中…(img-x4bm9OkU-1624947332611)]
[外链图片转存中…(img-MZ1rc7F6-1624947332612)]