JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
其中虚拟机栈、程序计数器、本地方法栈是线程私有的,也就是说每个线程都会维护自己的一份虚拟机栈、程序计数器、本地方法栈,而方法区和堆是所有线程共享的。
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
举例
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
将代码进行编译成字节码文件,我们再次查看 ,发现在字节码的左边有一个行号标识,它其实就是指令地址,用于指向当前执行到哪里。
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
通过PC寄存器,我们就可以知道当前程序执行到哪一步了
使用PC寄存器存储字节码指令地址有什么用?
在Jvm中是多线程环境,多个线程并发执行,CPU会不停切换各个线程,切换回来后,就需要知道接着从哪个地方开始。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。也就是说,方法的调用就对应栈帧的进栈,方法的返回就对应栈帧的出栈。
栈中可能出现的异常
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError 异常。
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 outofMemoryError 异常。
每个栈帧中存储着:
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及return Address类型。
成员变量(类变量,实例变量)、局部变量的区别
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
动态链接指向运行时常量池中该栈帧所属方法的引用,在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
为什么需要运行时常量池?
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
存放调用该方法的PC寄存器的值,如果有返回值,则将返回值压入调用者栈帧的操作数栈。
方法中定义的局部变量是否线程安全?
**
* 面试题
* 方法中定义局部变量是否线程安全?具体情况具体分析
* 何为线程安全?
* 如果只有一个线程才可以操作此数据,则必是线程安全的
* 如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全
* @author: 陌溪
* @create: 2020-07-06-16:08
*/
public class StringBuilderTest {
// s1的声明方式是线程安全的
public static void method01() {
// 线程内部创建的,属于局部变量
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是线程不安全的,操作的是共享数据
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同时并发的执行,会出现线程不安全的问题
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// StringBuilder是线程安全的,但是String也可能线程不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
总结一句话就是:如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法就是由c/c++编写的,用来与底层的系统比如操作系统交互的。
Java 8及之后堆内存逻辑上分为三部分:年轻代、老年代、元空间
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
为什么要把Java堆分代?不分代就不能正常工作了吗?新建对象具有朝生夕死的特点,而对于一些经过几次GC之后依然存活下来的对象,那么在接下来几次GC中也有很大可能不会被清理,比如数据库连接池的连接几乎不会被回收,对于这些对象我们不用每次都去扫描,而是把他们放在老年区,等到内存中实在放不下再去清理。
默认大小比例
Eden : S0 : S1 = 8 : 1 : 1
新生代 : 老年代 = 1 : 2
默认-XX:NewRatio=2,设置老年代与新生代的比例,表示新生代占1,老年代占2,新生代占整个堆的1/3
官方默认-XX:SurvivorRatio=8,设置Eden与S区的比例,表示Eden占8,但实际是6:1:1,要想是8:1:1,需要显式配置-XX:SurvivorRatio=8
-Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
-X:是jvm运行参数
ms:memory start
-Xmx:用来设置堆空间(年轻代+老年代)的最大内存大小
// 在Run/Debug Configuration中VM option中设置
-Xms10m -Xmx10m
// 可视化jvm内存工具
jvisualvm
class Main {
public static void main(String[] args) throws InterruptedException {
// 返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回Java虚拟机试图使用的最大堆内存
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + initialMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
List<int[]> list = new ArrayList<>();
while (true){
Thread.sleep(20);
list.add(new int[10000]);
}
}
}
栈溢出
-Xms:96M
-Xmx:96M
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main.main(Main.java:17)
我们一开始创建的对象,一般都是存放在新生代的Eden区,当Eden区满了后,会触发YoungGC。
将被GCRoot直接或间接引用的对象保留下来,存放到S0区,同时给每个幸存下来的对象设置一个年龄计数器,如果它在一次GC中幸存下来了,那么它的年龄就加1。
同时Eden去继续存放对象,当Eden区再次满了,又会触发YoungGC操作,此时GC将会把Eden和Survivor From中的对象进行一次收集,将存活的对象放到Survivor To区,同时年龄加1。
我们不断进行对象创建和垃圾回收,当Survivor中的对象年龄达到一个阈值的时候,将会触发一次Promotion晋升的操作,也就是将年轻代的对象晋升到老年代中。
Survivor From和Survivor To区是轮换的,为空的是To区,不为空的是From区,经过一次GC后From区变空成为To区,To区边为From区
特殊情况
代码演示
我们在main方法中不断创建对象,然后在jvisualvm中查看堆区的内存情况
新对象一直放在Eden区,放满之后触发YGC,由于对象都在使用着,所有对象不会被回收,因此,一部分对象放到To区,一部分直接到Old区,随着对象的不断创建,最后Old区和Eden区满了就触发OOM异常。
可以看到每到Eden区存储的峰值就会触发YGC,而后将幸存对象放在To区和Old区。
当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存。),触发MinorGC时会STW。
STW:stop the world,停止用户线程,启动GC线程清理垃圾
老年代的GC,出现了MajorGc,经常会伴随至少一次的Minor GC(但非绝对的,在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程),也会STW
触发Fu11GC执行的情况有如下五种:
说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些
可以在JVM启动参数中添加下列代码,打印GC日志信息。
-XX:+PrintGCDetails
Minor GC:新生代的GC
Major GC:老年代的GC
Full GC:整堆收集,收集整个Java堆和方法区的垃圾收集
混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
方法区
主要存放从class
文件中加载进来的类,JDK 1.8后这块区域改名为Metaspace
,即元数据空间,放的还是我们自己写的各种类相关的信息。