前言
时间过得真快,眨眼间已经毕业一年了,最近经常性的反思自己,发现这一年里竟然没什么让自己印象很深刻的东西,工作上了解了业务背景后,也没遇过一些对自己进步有帮助的难题之类的。仔细想想,在个人能力还是很初级水平的阶段,这样的处境是很危险的。所以最近会多做一些基础原理的知识整合,包括JVM、Java、Android方法的内容,一边扫盲一边加深理解。
一. jvm运行时内存的划分
JVM的内存模型结构如下:
按照是否线程共享
- 线程共享:方法区、堆
- 线程私有 :程序计数器,虚拟机栈,本地方法区
按照实际存储的内容
- 数据区:方法区,堆
- 指令区:程序计数器,虚拟机栈,本地方法区
看到这个结构,也能想的明白,用户数据(非指令,指令是和具体数据无关的,用来处理数据的、做计算 工作的操作数)是多线程共享的,如果说某些数据只能某个线程自己可见,那么多线程也就失去了意义。
1.运行时内存区域划分
1.1 程序计数器(Program Counter Register)-- 指令指针
- 1.1 作用
jvm中一块比较小的内存区域,指示当前字节码执行到哪一行,字节码的解释工作需要这个计数器来选取下一条被执行字节码指令,分支、循环、跳转、异常处理、线程恢复类的功能都要程序计数器来完成。 - 1.2 多线程
考虑在多线程情况下,这个区域的内存是线程私有的。 - 1.3 异常
计数器记录的是正在执行的虚拟机字节码指令的地址,假如这个区域内存泄漏,那么整个字节码的执行也就会无法进行,这种状态下,程序的异常处理,只要是程序层面上也都没法进行了。
1.2 虚拟机栈 – 记录方法信息和调用栈(函数调用栈,栈内每一个元素称之为栈帧,用javap命令查看栈帧信息进行理解)
- 描述的是方法执行的内存模型,每个方法在执行的同时会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口信息。
- 每一个方法从调用开始,就会创建一个虚拟机栈,方法结束调用栈也就会回收,相应的方法内的局部变量也都会被回收。
- 当调用栈过长,超过虚拟机定义的最大长度时,会抛出StackOverflowError,常见的情况就是定义了无递归出口的方法,会导致栈溢出
- 栈帧的内部结构组成:
- 局部变量表:存放编译期就能知道的类型,包括基本数据类型和对象引用,所需的内存空间大小也在编译期完成分配。也就是说进入一个方法时就可以确定,他在栈帧中需要占用多少内存空间,并且在方法运行期间局部变量表的大小不会再被改变。
- 操作数栈:函数在调用发起和结构本质上是其实入栈和出栈的动作。
- 动态链接:对多态支持的关键。
- 方法返回地址:记录方法调用结束的地址,即使是void类型的方法也有返回地址。
虚拟机栈的结构如下图:
之前刚看到虚拟机栈的时候,以为他本身就是存放的函数调用栈,后来发现,栈帧内元素的操作数栈,才是真正存放函数调用栈的结构。
对栈帧的理解
(1)每个线程都会创建自己的虚拟机栈,线程中每执行一个方法就创建一个栈帧。栈帧用于存储数据和结果,以及执行动态链接,方法的返回值和调度异常。
(2)当栈帧的方法调用结束时,无论结果是正常的还是错误的(比如未捕获的异常),都会被销毁。
(3)栈帧是从创建栈帧的线程的Java虚拟机堆栈中分配的,每个栈帧都有自己的局部变量表,自己的操作数栈,以及对当前方法类的运行时常量池的引用。
1.3 方法区
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。内存回收在这个区域和堆区是一样的规则。
- 运行时常量区
属于方法区的一部分,存储编译期生成的各种字面量和符号引用。
存储Class文件中的符号引用和直接引用。 - Class文件常量池
- 运行时常量池
1.4 本地方法区
和虚拟机栈的作用类似,虚拟机栈是为Java字节码提供服务支撑的,本地方法栈是为Native方法提供服务。会有StackOverflowError和OutOfMemoryError异常。
虚拟机规范并没有限制本地方法区使用什么语言,使用方法和数据结构,所以不同的虚拟机可以自己去实现这部分区域。注:Sun HotSpot虚拟机是将本地方法区和虚拟机栈合在一起了。
1.5 堆区
存放对象的实例,对象实例分配内存几乎都在这里进行。对象和数组都是在堆上分配内存的。是所有线程共享的区域,垃圾进行回收也主要在堆中进行,也被称为“GC堆”。
- Java堆还可以分为新生代和老年代
- 新生代:(2/3)默认比例eden:s0:s1=8:1:1
- 老年代:(1/3)
- 永久代:
2. 直接内存区
不是Java虚拟机规范中定义的内存区域,但是这部分内存被频繁的使用,并且内存不足时也有会有OutOfMemoryError异常出现。
直接内存的分配不受Java堆大小的限制。只受本机内存大小以及处理器寻址空间的限制。
3.总结各内存区域要点
运行时数据区域 | 职责 | 线程私有 | StackOverflowError | OutOfMemoryError |
---|---|---|---|---|
程序计数器 | 执行字节码的行号指示器 | 是 | N | N |
Java虚拟机栈 | Java方法在执行时的内存模型,存储局部变量表、操作数栈、动态链表、方法出口信息等 | 是 | Y | Y |
本地方法栈 | Native方法执行时的内存模型 | 是 | Y | Y |
Java堆 | 对象及数组所需内存的分配区域 | 否 | N | Y |
方法区 | 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 | 否 | N | Y |
4.实例代码分析内存的分布
虚拟机帮我们处理了内存的开辟、回收过程,我们在编码时,一般不需要过多的关注内存的东西,但是涉及到内存问题解决和优化 ,比如:内存泄漏,内存溢出,内存抖动,内存占用量偏高等,就需要我们对JVM对内存的管理有一个清楚的认识,才能提供一些有用的思路。
看下面一个简单的实例代码,搞清楚每个部分到底存放在哪。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//jvm为每一个线程创建单独的java虚拟机栈,为每一个方法创建一个栈帧,
// 结构关系:线程(栈帧(程序计数器,局部变量表,操作数栈,动态链接,返回地址))
// 栈帧内包含:程序计数器,局部变量表,操作数栈,动态链接,方法返回地址
//变量的存放位置?占用内存的大小怎么计算?
//第(1)部分
int a = 0;
int b = 1;
String str = "aaaaa";
int func = func();
Log.d(TAG, "onCreate: func-> " + func);
//类加载:load,linking,initialize
//栈和堆的关系?为什么说对象引用(变量)存放在栈区,对象的内容存放在堆区?
//对象存放在堆区占用大内存?
}
//第(2)部分
private int func() {
int a = 1;
int b = 2;
String str = "aaaaa";
//有对象的成员调用操作,也是存放在操作数栈里面?
func1();
return a + b;
}
//第(3)部分
private void func1() {
int a = 1;
int b = 2;
String str = "aaaaa";
}
}
首先,我们通过对JVM的理解,能知道有下几个规则:
(1)方法区和堆区是内存共享的,java虚拟机栈、本地方法栈、程序计数器是线程私有的;
(2)java虚拟机栈是由若干个栈帧组成的,按(1)可以得知:根据java虚拟机栈是线程私有的,可以知道每个线程都有单独的java虚拟机栈。
栈帧的结构在1.2已经提到过,是由程序计数器、局部变量表、操作数栈、动态链接和返回地址组成。每调用一个方法,都会创建一个栈帧,当然这个栈帧肯定是由线程所在的java虚拟机栈所创建的。
(3)变量存放在栈中,对象的实例存放在堆中。为什么这么说?请看后续的分析。
按上面的规则,可以将上述的代码内存分布描述如下:
二. 对象的四种引用类型
1.强引用
Object strongRef = new Object();
程序运行过程中就不会被回收, 当内存不足时会抛出OutOfMemoryError错误, 也不会选择回收强引用对象来缓解内存不足, 一般这种强引用从逻辑上确定是可以回收了,可以赋值为null, 提高被GC回收的可能性.
strongRef = null; //注意:并不是这样做了就立马被回收,只是能被回收了,具体回收的时机由GC决定
2.软引用: SoftReference
- 2.1 软引用特性
如果一个对象只有软引用, 只要内存足够, 就不会回收它. 如果内存不足了, 就会回收这些只有软引用的对象. 可以看出软引用的回收时机是由内存的使用情况决定的.
软引用在内存充足的时候对象不会被回收, 也就是说内存充足的情况下, 即使对象只有软引用, 对象也不会被回收. 所以软引用也能解决内存泄露问题, 但是相对来说对象回收的周期更长, 因为某个对象仅有软引用时, 要等到内存不足才会被回收.
软引用能感知内存不足的发生, 所以适用于内存敏感的使用场景.
- 2.2 软引用和引用队列的结合使用
首先看SoftReference的构造方法, 有两个:
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
/**
* 当软引用对用的对象被回收了,就会将这个软引用对象加入到队列中
*/
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
使用方式:
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abcd");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
System.gc();
System.out.println(softReference.get());
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference);
3.弱引用: WeakReference
对象只有弱引用, 不会导致对象在被回收时, 因为还有引用关系不能被GC回收. 相反, 如果GC在扫描对象只有弱引用时, 不管内存是否充足, 都会回收该对象内存. 但是GC的线程优先级很低, 不会很快扫描到这种情况的对象引用.
和软引用相比, 若引用的生命周期很短暂.
弱引用也可结合引用队列使用:
public WeakReference(T referent) {
super(referent);
}
// 当弱引用对应的对象的被回收了,jvm会将该弱引用加入到引用队列中
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
使用:
Object object = new Object();
ReferenceQueue<Object> weakReferenceQueue = new ReferenceQueue<>();
WeakReference<Object> weakReference = new WeakReference<>(object, weakReferenceQueue);
4.虚引用: PhantomReference
虚引用不会决定对象的生命周期, 如果一个对象只有虚引用, 那么它在任何时候都有可能被回收. 虚引用必须和一个引用队列联合使用.
和软引用,弱引用不同的是虚引用必须和引用结合使用, 可以看到它只有一个构造方法:
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
使用:
Object phantomRefObj = new Object();
ReferenceQueue<Object> phantomReferenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(phantomRefObj, phantomReferenceQueue);
System.out.println(phantomReference.get()); //null
一般不用虚引用来做实际的工作, 可以用来跟踪垃圾回收器的活动.
5. 四种类型引用特性总结
引用类型 | 获取对象方式 | 是否回收 | 是否内存泄露 |
---|---|---|---|
强引用 | 直接使用 | 不回收 | 是 |
软引用 | softRef.get() | 内存不足时回收 | 否 |
弱引用 | weakRef.get() | 只存在弱引用时回收 | 不可能内存泄露 |
虚引用 | null | 任何时候都有可能被回收,如果没有引用 | 否 |