Java内存模型学习

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限制


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值