零蚀
java 内存区域
-
前言
对于C/C++来说,程序员像是一个公民,既有管理内存的权利,也有维护内存义务,相较于Java,在虚拟机的自动内存管理机制下,我们不需要过多的去接受free/delete的处理,每当我们new出一个堆内存时,虚拟机都看似完美的处理了内存问题,但是如果出现问题,我们的处理难度,就异常难搞了。
-
运行时的数据区域
-
区域简介
-
程序计数器(Program Counter Register): 它是线程私有的,在java虚拟机中,是有多个线程来执行字节码指令的,多线程就是通过多个线程轮番切换,来进行计数。为了让线程切换后都能执行到正确的位置,所以每个线程都需要一个独立的程序计数器,这些计数器相互不影响,独立存储,我们称这类的内存是线程私有的,我们的跳转,循环,分支,异常处理 etc. 都是要依赖程序计数器。当然如果我们的执行的是本地(native)方法,程序计数器为NULL。
-
java 虚拟机栈 (java Virtual Machine Stack): 它是线程私有的,声明周期和线程相投,每个方法被执行的时候,虚拟机都会同步的创建一个栈帧,用来存储方法里局部变量。
-
本地方法栈 (Native Method Stacks): 与java虚拟机栈类似,虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈是为了执行本地(native)方法服务。
-
Java 堆 (Heap): 首先堆事内存管理中最大的一块,它是所有线程所共有的,在虚拟机创建的时候创建;其次,几乎所有的对象都是在这里分配内存对象(排除逃逸分析etc.,可见NO.2 java gc回收机制提及,后续也会说明)。Java堆是垃圾回收机制所管理的区域,因此也叫GC堆(Garbage Collected Heap,“垃圾堆”),暂时不谈GC堆的分代设置。最后 Java 堆的大小是可以设置的,当大小超过限制时会抛出
OutOfMemoryError
。 -
方法区 (Method Area) : 也叫做“非堆”(Non-Heap),它是线程共享的,它用于存放虚拟机已经加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存。其中还包括 运行时常量池(Runtime Constant Pool) 类具有一个常量池表,用于存放类的各种字面量(右值)和符号引用。
-
直接内存(Direct Memory):它不是运行时数据区的一部分,它是在JDK1.4加入的一种 NIO (New Input/Output)类,引入了一种基于Channel和Buffer的I/O方式,他可以使用native函数库直接使用堆外内存,他的作用是某些情况下提高性能,避免在Java堆和native堆之间来回复制数据。当然内存就会有OutOfMemoryError异常。
-
-
Java堆中对象的分配、布局、访问
先弄明白栈、堆、常量池的关系,就图说明没什么好说的。
-
类的创建:类创建首先在接收到new这个指令后,检查这个指令在常量池中是否能定位到这个类的符号引用。通过这个符号引用,可以得知这个类是否给加载、解析、初始化过,如果没有就会执行类加载的过程。如果有虚拟机就会堆新生对象进行内存分配,对象所需的大小在类加载完就可以确定,然后虚拟机会从堆内存上划出一块内存。
-
类的创建,内存分配方式,一般有以下两种:
- 指针碰撞(Bump The Pointer):在已经使用内存和空闲内存之间有一个指针作为分界线,这个时候内存块是规整的,指针一边是空闲,一边是使用过的,当内存分配的时候指针会向内存块空闲一侧移动对应大小的距离。这种方式简单高效,但是内存一定要是规整的。
- 空闲列表(Free List): Java堆不是规整的,已用的内存和空闲的内存交错在一起,那么虚拟机就要有一个列表,记录有哪些是已用内存,哪些是空闲内存他们有多大,然后划分一块足够的内存给创建的对象,并刷新信息列表。
-
-
对象的内存布局
对象在堆里的内存布局可以分为三个部分:对象头,对象数据,对其填充。
-
对象头:对象头又叫“Mark Word”一部分是左侧的运行时数据(HashCode、GC分代年龄……),一部分是右侧的类型指针,用来制定这个类是哪个类的实例,如果是数组的情况下,还会记录数组的擦痕高长度。
-
对象值:这是对象存储的真正的有效信息。
-
对齐填充:因为对象要保持是8字节的整数倍而出现的占位符,没有特殊含义(对象头已经被设置为8字节的整数倍)。
-
-
对象的访问
当我们创建了对象,后续的使用方式是,通过栈上面的reference来定位对象在堆里的地址。具体的访问方式是由虚拟机来定的,目前访问方式有两种,句柄和指针:
-
句柄访问:java堆中会划出一个句柄池,里面存着对象的句柄地址,里面记录了对象的实例/类型地址。它的优点是当实例对象发生地址移动时,只会改变句柄的指针。
-
指针访问:节省了开辟句柄池的空间,直接访问对象地址。它的速度快。
-
-
堆OOM(OutOfMemoryError)
- 堆内存溢出
我们可以设置虚拟的分配堆内存为一个固定值,然后进行报错测试。
这里设置了虚拟机的堆内存分配的最大值和最小值都为5M(堆的内存下限受虚拟机控制),这样时为了固定堆的大小防止他自己扩容,然后添加OOM异常抛出(HeapDumpOnOutOfMemoryError这个会给VM内部的异常做一个异常抛出,让我们能分析到VM内部原因)。
-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
java代码
public class JavaOOM { public static void main(String[] args) { List<Demo> list=new ArrayList<>(); while(true){ list.add(new Demo()); System.out.println(list.size()); } } } class Demo{ } // -------------print------------- Dumping heap to java_pid2195.hprof ... Exception in VM (AttachListener::init) : java.lang.OutOfMemoryError: Java heap space Heap dump file created [9775076 bytes in 0.098 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
-
栈OOM
设置栈的大小为144K(栈的内存下限受虚拟机控制)
-Xss144k
然后通过递归不断压栈。
public class JavaOOM { static int a=0; public static void stackOOM(){ System.out.println(a); stackOOM(); } public static void main(String[] args) { stackOOM(); } } // -------------print------------- Exception in thread "main" java.lang.StackOverflowError
不断创建线程对象,也是会导致栈内存溢出的。
GC(Garbage Collection)
-
前言
垃圾回收之前,gc堆要判断哪些对象是‘活的’,哪些对象已经‘死了’。主要的方法分为两种,分别为引用计数算法 & 可达性分析算法。
-
引用计数算法: 在对象中添加一个引用计数器,当引用它时,引用+1,当引用失效时候,引用-1,当引用数值为0的时候,说明这个对象不可被使用,这种方法在Python中被使用,但是Java中却没有使用它来管理内存。因为这不能解决相互引用的问题
B.param=A;A.param=B
。 -
可达性分析算法:根据GC Root 是否和对象存在引用链关系,来决定是否可用,如果GC Root和对象间引用链相连表示对象可用,没有引用链关系表示不可用对象,如下图所示:
jdk1.2以后又添加了四种引用,分别是‘强引用’,‘软引用’,‘弱引用’,‘虚引用’四种,详细可看 Android篇[🔗 NO.2 java gc回收机制] ,这里值得一谈的是,一个对象持有虚引用不会对其生存时间有任何的影响,它的唯一作用当对象被回收时会有通知。
垃圾回收中,对象的自我救赎:其实垃圾回收的时候,对象并没有立刻被回收,这个时候即使gc堆检测到这个对象已经没有引用链接关系的时候,也不会立刻死刑,因为这个时候虚拟机还会检查这个对象的finalize(),如果没有被这个方法覆盖,或者已经调用过了,就直接死刑,如果没有,回调用finalize方法,这个对象的唯一逃生机会,当然我们可以复写这个方法,添加引用,但是不一定就可以帮他逃生,主要还是看VM的心情。
-
-
分代
Java堆里一般分为 新生代 & 老年代:
- 新生代(Young Generation):新声代在垃圾回收中,会大批死去,每次回收中存活的对象逐步变为老年代。
- 老年代(Old Generation):老年代很难被回收,同时目前的老年代也不会和新生代产生跨代引用,防止新生代也难以被回收。
回收的方式也不是对所有的内容进行全盘的检查,是否回收,而是有以下内容构成:
-
部分收集:
- 新生代回收(Minor GC/Young GC):
- 老年代回收(Old GC):(CMS会单独收集老年代)
- 混合回收(Mixed GC):整个青年代和部分老年代回收
-
整体收集:对整个java堆和方法区的垃圾回收
-
回收的算法
-
标记-清除算法(Mark-Sweep):算法分为标记和清除两个过程,标记-清除会标记 清理/存活对象,来确定是否被GC清理掉,他也是最基础的垃圾回收的手段。这种清除手段会有以下问题:清除效率不稳定,因为如果内存中有大量的对象要被标记&清除,会有大量的清除、标记动作。其次会产生大量的内存碎片,如下图所示。
-
标记-复制算法:又称半区复制的垃圾回收算法,目的是为了解决标记清理的效率问题,它将可用内存等大的两块内容,每次只是用一块,将这一块内容用完了,就讲还活着区块复制到另一个内存空间,并将这边的内存空间清空,从而解决清除的效率问题,但是问题是,空间的大小被缩减了一半,空间浪费过大的代价。(如果内存不足,会将外部内存)
- 标记-整理算法: 主要解决标记复制时存活对象一直存活而导致的效率低,空间浪费的问题,标记-整理算法的标记过程和标记-清除的标记过程是一致的,只是后续的过程不是直接清理,而是先对存活对象进行移动,向空间的一端移动,然后清理掉边界以外的内存。而应对老年代不会用这方法,因为其中老年代100%的存活率。并且这种移动式回收方式,移动活着的对象本身就是一个极耗性能的操作。
内容参考自《深入理解java虚拟机》,无商业目的
-
🔗 前言
🔗 Android 知识栈
🔗 JVM 快速排序篇
🔗 NO.1 OpenJDK 前言
🔗 NO.3 垃圾收集器&ClassLoader
🔗 NO.4 原子&线程