https://blog.csdn.net/wei_lei/article/details/70738379
https://blog.csdn.net/bingduanlbd/article/details/8363734
下面我根据《深入理解Java虚拟机》(周志明著),对上面的博客作一下补充和总结。
内存区域的划分
Java虚拟机管理的内存也叫运行时数据区域,Java虚拟机规范一共将内存划分为5个块,虚拟机栈、本地方法栈、程序计数器、Java堆、方法区。其中,虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程而生,随线程而灭;而Java堆、方法区则是线程共享的,随着虚拟机进程的启动而存在。
-
各个内存区域的作用
上面博客整理得很详细,我就不多说了。
1)程序计数器
线程执行相关
2)栈内存
分为本地方法栈与虚拟机栈
本地方法栈:存放native方法相关的信息
虚拟机栈:存放java方法相关的信息,每个方法(不包括native方法)执行的时候都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。在函数中定义的一些基本类型的变量和对象
的引用都是在函数的栈内存中分配。当在一段代码库中定义一个变量是,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java就会自动释放为该变量分配的内存空间。(所以局部变量占点内存没有关系,方法执行完它就行释放。)
3)堆内存
堆内存用于存放所有由new创建的对象(包括该对象其中的所有的非静态成员变量)和数组。
4)方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、和编译器编译后的代码(也就是存储字节码文件。.class)等数据
对象
对象的创建
Java语法new就是创建一个对象,虚拟机是如何处理这个
指令的:
1)类加载
首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用的代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
2)分配内存
内存分配方式
如何为新生的对象分配内存?有2种方式:
- 指针碰撞
这种方式有一个大前提:假设Java堆中内存是绝对规整的,所有用过的内存放一边,空闲的内存放在另一边,中间放着一个指令作为分界点的指示器,分配内存就是就是将指针向空闲内存移动与对象大小相等的距离。 - 空闲列表
维护一个列表记录哪些内存可用与空闲。
而Java堆是否规整,是由Java虚拟机所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的并发问题
Java虚拟机中创建对象是非常频繁的行为,即使是使用指针碰撞方式来分配内存,在并发情况也并不是线程安全的。如给对象A分配内存,指针还没有来得及修改,对象B又使用了原来的指针来分配内存的情况。解决方案有2种:
1)对内存分配的动作进行同步处理
CAS配上失败重试
关于什么是CAS:https://blog.csdn.net/tanga842428/article/details/52742698
2)TLAB(本地线程分配缓冲)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。
所以以前编程就听见有人说创建线程十分消耗内存,这里算是明白了,这也是为什么有线程池来复用线程的原因。
3)对象初始化
Java对象的字段都有默认值,就是Java虚拟机在这里为对象初始化的。
4)对象设置
对象头:类的元数据信息、对象的哈希码、对象的GC分代年龄
对象的内存布局
在Hotspot虚拟机中,对象在内存中存储的布局分为3块区域:对象头、实例数据、对齐填充。
- 对象头
对象头包括2部分信息,第一部分用于存储对象自身的运行时数据(Mark Word);第二部分是类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例,但不是所有的虚拟机都有类型指针。 - 实例数据
实例数据是对象真正存储的有效信息,也是代码中定义的各种类型的的字段内容。 - 对齐填充
保证整个对象的大小是8的整数倍,假如实例数据没有对齐,对齐填充部分会自动补齐。
对象的访问定位
取决于虚拟机的实现,主流的访问方式有句柄和直接指针两种。
-
句柄
Java堆中划分一部分内存来作为句柄池,reference会存储对象的句柄地址,而句柄包含对象实例和类型数据各自的地址信息。
image.png
使用句柄的好处就是对象被移动,只要改变句柄指向对象实例的指针,reference不需要作改变。
-
直接指针
reference直接指存储对象的地址
image.png
使用直接指针的好处就是访问速度快,节省一次指针定位的时间开销。因为对象的访问在Java中十分频繁,Sun Hotspot虚拟机就是采用的这种方式访问对象。
异常
分析Java虚拟机的异常,是为了让我们在遇到这些异常时,能清晰地定位到可能产生这些异常的代码。
OutOfMemoryError异常
java虚拟机各个内存部分,除了程序计算器之外,都会抛出OutOfMemoryError异常。
-
Java堆溢出
不断的创建对象,并且保证对象被引用(不被垃圾回收器回收),当对象数量达到最大堆的容量限制就会抛出内存溢出异常。在Java堆中,这个异常其实包涵两个方面的层义:内存泄漏(Memory Leak)和内存溢出(Memory Overflow)。
-
内存泄漏(Memory Leak)
对象被引用,垃圾回收器无法回收,导致内存溢出的原因之一。堆内存中的
长生命周期对象持有短生命周期对象的强/软引用
,尽管短生命周期的对象已经不再需要了,但是长生命周期对象持有它的引用而导致不能被回收,这就是Java内存泄露的根本原因。 -
内存溢出(Memory Overflow)
Java堆再也无法为新创建的对象分配内存了,是结果。
制造异常
public class HeapOOM {
public static class OOMObject{
}
public static void main(String[] args){
List<OOMObject> list = new ArrayList<OOMObject>();
while (true){
list.add(new OOMObject());
}
}
}
在Android端运行HeapOOM.main(null),等待一段时间抛出异常:
java.lang.OutOfMemoryError: Failed to allocate a 68706640 byte allocation with 16777216 free bytes and 36MB until OOM
这个异常是由Android虚拟机抛出的,所以和作者讲的Java虚拟机输出日志有所不同。之所以直接运行main方法,是我没有找到限制堆容量大小的方法,等了很久也没有异常产生。
-
栈溢出
当线程请求的栈深度大于虚拟机所允许的最大深度时,就会抛出StackOverflow异常。
当虚拟机扩展栈时,无法申请到足够的内存,就会抛出OutofMemoryError异常。
制造StackOverflow异常
写一个死循环
public class StackOverflow {
public static void main(){
Log.e("CZ", "main: test");
main();
}
}
在Android端运行StackOverflow.main(),等待一段时间抛出异常:
java.lang.StackOverflowError: stack size 8MB
制造OutofMemoryError异常
在Java栈中,一般很难出现OutofMemoryError异常,尝试用下面代码:
public class ThreadTest {
private static void dontStop(){
while (true){
}
}
public static void stackLeakByThread(){
while (true){
Thread thread = new Thread(){
@Override
public void run() {
Log.e("cz", "run: ");
dontStop();
}
};
thread.start();
}
}
}
但是上面的代码,最终会导致adb offline,手机卡死。由于Java线程会和操作系统的内核线程产生关联,导致系统卡死,应用也不会崩溃。
https://blog.csdn.net/qq_27035123/article/details/77651534
-
方法区溢出
制造OutofMemoryError异常
String的intern()方法:判断运行时常量池是否存在这个常量,不存在则添加。利用这个方法可以制造出异常
public class RuntimeConstantPoolOOM {
public static void main(){
List<String> list = new ArrayList<String>();
int i = 0;
String pre = "ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
"ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
"ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
"ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" +
"ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss";
while (true){
Log.e("CZ", "main: test");
list.add(String.valueOf(pre + (i++)).intern());
}
}
}
这个等到出现异常会需要很久的过程,抛出异常如下:
java.lang.OutOfMemoryError: Failed to allocate a 1576 byte allocation with 4291168 free bytes and 4MB until OOM; failed due to fragmentation (required continguous free 131072 bytes for a new buffer where largest contiguous free 40960 bytes)
另外对String的intern()这个方法再作一下补充,JDK1.6和JDK1.7的底层实现不一样。
制造OutofMemoryError异常2
通过一些动态代理的框架,循环的加载增强的类,也能在方法区产生异常。
垃圾收集器
GC(Garbage Collection)的概念并不是Java独有,很多年前人们就在思考GC要做的3件事情:
1)对象已死吗?
2)什么时候回收?
3)何时回收?
对象已死吗
- 引用计数法
优点:实现简单,效率高。
缺点:无法解决对象循环引用的问题。 - 可达性分析算法
GCRoots对象到对象是否可达
引用
《深入理解Java虚拟机》(周志明著)书中第65页对这个部分做了很详细的说明,Java引用在JDK1.2分为强、软、弱、虚4种引用,强度依次减少。
- 强引用
直接用Object obj = new Object()这样形式new出来的对象,如果引用还在,垃圾回收器永远不会回收。 - 弱引用
使用SoftReference创建,在系统发生内存溢出之前会回收软引用关联的对象,如果回收之后还没有足够的内存才会抛出异常。 - 软引用
使用WeakReference创建,当垃圾回收器工作时,无论内存是否紧张,都会回收软引用关联的对象。 - 虚引用
使用PhantomReference创建,不会对对象的生存时间造成影响,也无法通过虚引用获取对象实例,虚引用只起到对象被垃圾回收器回收时收到一个系统通知的作用而已。
4种引用的应用场景:
https://blog.csdn.net/qq_40434646/article/details/92569183
https://www.jianshu.com/p/825cca41d962
finalize方法
当对象不可达时,不代表它就一定会死,看书中第66页对这个方法的说明。
https://www.jianshu.com/p/0618241f9f44
方法区(永久代)回收
主要回收废弃常量和无用的类
垃圾收集算法
- 标记-清除算法
效率不高,标记、清除的过程的效率不高。
空间问题,产生大量的不连续的内存碎片。 - 复制算法
优点:解决了内存碎片问题
缺点:内存只能使用一半,实际虚拟机并不是1:1划分。
新生代分配担保机制,老年代。 - 标记、整理算法
在标记-清除算法基础上,最后不直接清除回收对象,而是先整理移动存活对象到内存一端,再清除另一端,这样就没有内存碎片了。 - 分代收集算法
将Java堆分成新生代和老年代,新生代采用复制算法,老年代采用标记-清除算法或者标记、整理算法。
Hopspot虚拟机算法实现
GC卡顿
为了保证可达性分析的准确性,虚拟机会停顿所有的Java线程。
概念:OopMap、安全点、安全区域
垃圾收集器
内存回收的具体实现者。
网友评论