0、JVM内存模型
程序技术器 + 虚拟机栈(线程栈) + 本地方法栈 + 堆 + 方法区(永久代)
为什么需要程序计数器?
答:就单CPU来说,线程执行需要首先获取CPU时间片,并且线程不是一次性执行完,而是存在线程切换(OS调度)。那么线程后面一次执行肯定是在前一次基础上进行,因此就必须记住前一次执行位置,即程序技术器设计的初衷。
哪些是线程私有区域?
答:程序技术器+线程栈 + 本地方法栈
什么是本地方法栈?
答:JVM调用操作系统方法所使用的栈
什么是栈深度?
答:线程内每次方法调用对应着在线程栈中入栈,但是栈深度也有限制(1000?),超过则报StackOverflowException
哪些是线程共有区域?
答:堆 + 方法区(永久代)
栈中保存哪些信息?
答:局部变量(基本类型变量 + refrence) + 方法参数
方法区保存哪些信息?
答:Class元数据(访问修饰符、字段等) + 静态变量 + 常量池
方法区与永久代?
JVM规范把方法区描述为堆的“逻辑结构”,沿用堆分带概念,被叫做“永久代”,但它又不属于堆,属于“非堆”
最大线程数取决于什么?
答:OS给进程分配内存有上限Max(32位和64位机器上限不一),线程栈大小(一般2M?)*线程数量=Max-堆大小-方法区大小-程序计数器大小(占用空间可以忽略不计)
常见的JVM异常分别会出现在哪些区域?
答:StackOverflowException——虚拟机栈,本地方法栈
OOM,heap space
OOM,perm space
常量池?静态常量池与运行时常量池?
答:静态常量池指class编译确定的信息,包括字面常量、类型信息、方法信息
运行时常量池是指Class被装载到内存后,class内部的常量被拷贝到JVM的方法区
常说的常量池指的是运行时常量池
常量池在jdk1.6/1.7/1.8版本中的流转?
答:jdk1.6中,Hotspot将GC分带收集扩展到了方法区,用永久代实现了方法区,目标是对常量池及卸载的类进行收集
jdk1.7中,仅仅是将常量池从永久代移除,放到了堆中
jdk1.8中,彻底消灭了永久代,常量池放到一个本地内存区域,区别于堆(相互隔离),叫做元空间
(Tip:使用了jdk1.6的应用在部署时需要小心设置MaxPermSpace,如果引用了大量第三方jar或有很多动态类生产,则会有OOM Perm space的风险;将常量池移到堆或本地内存后,同样的场景一般不会再报OOM了)
1、JVM内存主要由线程栈和堆两部分组成,JVM支持多线程,并为每个线程分配一个线程栈。
2、每个线程栈都有一个方法调用堆栈,用于追溯各个方法的逻辑调用过程,每个方法中会创建很多局部变量,尽管不同线程会执行同样的方法,但是每个线程会有不同的局部变量拷贝,8种基本数据类型(boolean, byte, short, char, int, long, float, double)变量都存储在线程栈中。所有对象均在堆中创建,而不管对象是局部变量还是成员变量。
3、线程栈中的局部变量不能被不同线程共享,堆中的对象可以被多个线程共享
public class MyRunnable implements Runnable{
public void run() {
methodOne();
}
public void methodOne() {
int localVariable1 = 45; // 局部变量,存储在线程栈中
MySharedObject localVariable2 = MySharedObject.sharedInstance; // localVariable2是对象引用,局部变量,存储在线程栈中,并指向堆中具体对象
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99); // localVariable1是对象引用,局部变量,存储在线程栈中,并指向堆中具体对象
//... do more with local variable.
}
}
public class MySharedObject {
//static variable pointing to instance of MySharedObject
public static final MySharedObject sharedInstance = new MySharedObject(); // 在堆中创建一个对象,并由sharedInstance引用
//member variables pointing to two objects on the heap // sharedInstance,object2,object4,member1,member2都是成员变量,存储在堆中
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
public long member1 = 12345;
public long member2 = 67890;
}
法则:
(1)局部变量,并且是基本类型,则存储在线程栈中
(2)局部变量,并且是对象类型,则变量本身存储在线程栈中,所指对象在堆中分配
(3)对象方法中的局部变量存储在线程栈中,即使该对象在堆中存储
(4)对象成员变量在堆中存储,不管它是基本类型还是引用类型
(5)静态类变量在堆中存储(也就是说堆中不是只存储Object)
4、硬件内存模型
现代计算机大多拥有多个CPU,每个CPU拥有多个核,由于一个CPU仅能同时执行一个线程,借助多个CPU就能实现多个线程【并行】执行。为了提高CPU利用率,要想法设法提升数据存取速度,因此设计了寄存器、CPU内存缓存、主存,由左至右存取速度递减。
5、JVM内存模型与硬件内存模型
硬件内存模型没有堆和栈的概念,JVM内存中线程栈和堆均是在主存中分配。但是线程栈和堆中数据可能同时出现在主存、CPU缓存、寄存器中。
6、当对象和变量出现在不同内存区域时,多个线程共享变量会导致一些问题
(1)共享变量可见性:线程A读取主存中共享变量,并执行计算(修改),但还没有将修改写到主存中,此时线程B也读取了共享变量,但读取的不是最新版本
解决方法:使用volatile关键字
- 每次修改volatile变量都会同步到主存中
- 每次读取volatile变量的值都强制从主存中读取最新的值(强制JVM不可优化volatile变量,如果JVM优化,变量读取会从CPU缓存中读而不是主存中)
- 注意:volatile解决的是多线程之间共享变量的可见性问题,并不能保证非原子性操作(i++,++i)的多线程安全问题
- 为什么会存在线程安全问题:即下文所说的并发修改情形
举例说明volatile变量线程安全问题
public class TestMain {
private static volatile int count = 0;
private static final int MAX_VALUE = Integer.MAX_VALUE;
private static AtomicInteger count1 = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(new DecreTest());
thread1.start();
for(int t=0;t<MAX_VALUE;t++){
count++;
}
thread1.join(); // 等待子线程执行完,再继续往下执行
// while (thread1.isAlive());
System.out.println("result: count=" + count);
}
static class DecreTest implements Runnable {
@Override
public void run() {
for(int w=0;w<MAX_VALUE;w++){
count--;
}
}
}
}
输出结果每次都不一样,期望是为0
使用原子性操作包装类可以解决线程安全问题
public class TestMain {
private static volatile int count = 0;
private static final int MAX_VALUE = Integer.MAX_VALUE;
private static AtomicInteger count1 = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(new DecreTest());
thread1.start();
for(int t=0;t<MAX_VALUE;t++){
count1.incrementAndGet();
}
thread1.join(); // 等待子线程执行完,再继续往下执行
// while (thread1.isAlive());
System.out.println("result: count=" + count);
}
static class DecreTest implements Runnable {
@Override
public void run() {
for(int w=0;w<MAX_VALUE;w++){
count1.decrementAndGet();
}
}
}
}
每次执行结果都为0,说明是线程安全的
另外一种保证线程安全的方式就是加锁,即下文所说的并发修改时,使用synchcronized保证同步,AtomicInteger和synchcronized内部实现不一样,前者不是通过加锁实现,效率更高
(2)并发修改:线程A读取并修改共享变量同时,线程B也读取并修改了共享变量,最后写入主存后,只保留了一个线程的修改
解决方法:使用synchronized关键字
使用synchronized修饰的代码块保证同一时刻只能有一个线程能够进入,并且在此代码块内的共享变量直接从主存中读取,当线程执行完该代码块准备退出时,立即将修改写入主存,无论共享变量是否被声明为volatile。
(3)什么时候只用volatile就可以了
当只有一个线程读写共享变量,其它线程只读共享变量,使用volatile就足够
如果存在多个线程【既可能读又可能写】共享变量,此时仅仅使用volatile就不行,必须加上synchronized限制