先来说一说一些别的东西。
1.Java内存分配:
-
对象优先在Eden区分配
-
大对象直接进入老年代
大对象其实就是需要大量连续空间的Java对象,例如:很长的字符串以及数组。
虚拟机提供了一个PretenureSizeThreshold参数,大于这个参数设置值的对象直接在老年代分配空间。这样主要是为了避免在Eden区和Survivor区发生大量的内存复制。 -
长期存活的对象进入老年代
对象晋升到老年代的年龄阈值,可以通过参数MaxTenuringThreshold来观察,一般默认年龄到达15岁后,进入老年代。
2.Java动态对象的年龄判定:
如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,则大于等于该年龄的对象就可以直接进入老年代,不需要等到15岁。
3.JDK中常用的JVM检测工具:
主要是命令行工具:
-
jps:虚拟机进程状态工具
jps -l :可以列出当前正在执行的虚拟机进程,并返回这些进程的唯一ID在打开IDEA之前:
打开IDEA后:
-
jmap:内存映像工具
jmap -heap ID:显示Java堆的详细信息
jmap -histo ID:显示对象的统计信息
jmap -dump:生成Java堆的转储快照 -
jstack:Java堆栈跟踪工具
用来生成虚拟机当前线程快照,从而定位线程出现长时间停顿的原因。
jstack -l:显示关于锁的附加信息
4.Java内存模型:(JMM)
1)目的:屏蔽各种硬件和操作系统的内存访问差异,实现一致的访问效果。
2)主要目标:定义变量的访问规则(不包含局部变量和方法参数),其实就是如何将变量从内存中取出以及如何将变量写入内存等细节。
3)线程、工作内存与主内存的交互关系:
- 每条线程都有自己的工作内存,不同线程之间无法访问对方工作内存中的变量
- 所有的变量都必须存储在主内存中,线程中的工作内存不过是保存了该线程使用到的变量的副本
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递需要通过主内存来完成。
我们来画图举例说明:
当一个线程需要对一个共享变量进行读写操作的时候:
第一步:将主内存中的共享变量拷贝到该线程自己的工作内存中;
第二步:在自己的工作内存中进行修改(例如:赋值)
第三步:将修改后的共享变量的值写回到主内存中(即更新该共享变量的值)
在实现以上操作时,每条操作必须都是原子的、不可再分的。
- 为什么要保证原子性呢?
因为这样可以保证线程是安全的。
5.那我们来想一想为什么会线程不安全呢?
假如现在有两个线程同时从主内存中拿到了一个共享变量(假设值为1),并且都对它进行操作(假如这个操作是+1),我们预期的结果是这个共享变量的值是3,因为进行了两次相加操作,但结果是2,因为实际上写回主内存的值只进行了一次相加操作。
6.只有当以下三个特性同时满足的情况才是线程安全的:
- 原子性:一个或多个操作在CPU的执行过程不会被打断。
例如:基本数据类型的访问(读写操作)是满足原子性的。
但加减操作不具备原子性。 - 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知此修改。
拿我们刚才举得例子来说:线程1拿到值后,进行加一操作值变为2;此时若满足可见性,则线程2会立即得知此修改,再进行加一操作变为3,此时写回主内存的值就是我们期望的值,解决了线程不安全的问题。
其实多线程同步就是保证了原子性和可见性。 - 有序性:即就是按照代码的顺序依次进行。
一段代码可能在我们的线程内看是有序的,但是在其他线程看来就是无序的。
JMM具有先天的有序性,即不用任何操作就能保证代码是有序的,这称为JMM的happens-before原则。若不满足该原则,JVM会进行指令重排。
7.volatile关键字:
volatile是轻量级同步关键字,用来修饰变量。
- volatile关键字用来保证可见性(每个线程对变量的修改对其他线程都是立即可知的)
- volatile关键字可以保证有序性:volatile变量前的语句一定优先于volatile变量之后的语句执行。