Java和C++之间存在一堵由内存动态分配和垃圾自动回收所组成的“围墙”,墙外的人想进来,墙内的人想出去。
Java和C++之间存在一堵由内存动态分配和垃圾自动回收所组成的“围墙”,墙外的人想进来,墙内的人想出去。
1. 对比
- 对于C++,C程序开发人员,他们需要亲自管理内存,从内存生命周期的开始到结束。亲力亲为。
- Java中存在垃圾自动回收机制,不容易发生内存泄漏,但是,如果出现了问题,同时对GC原理不熟悉,排查错误将会是一项艰难的工作。
2. 运行时数据区域
JVM虚拟机吧内存划分为5个不同的数据区域。分别为线程私有
的(PC Register程序计数器,VM Stack 虚拟机栈,Native Method Stack 本地方法栈)以及线程共享
的(Method Area 方法区,Heap 堆)。其中PC Register是唯一不会报OOM的区域。
- 程序计数器:当前线程所执行的字节码的行号计数器。如果执行的是本地方法,该值为undefined。
- 虚拟机栈:生命周期与线程同步,描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:功能与虚拟机栈类似,只不过它执行的是native方法服务。
- 方法区:用于存储被虚拟机加载的类信息、常量、静态变量等。通常把方法区称为堆的一个逻辑部分,还有另一个别名叫‘No-Heap’非堆,JDK8中的HotSpot虚拟机之前称之为(Permanent Generation)永生代。
- 堆:内存中最大的一块,大量的实例对象都在此存放,同时该区也是GC的主战场。根据垃圾回收可细分为新生代和老年代,新生代分为Edon,From Survivor和To Survivor。
虚拟机对象
当虚拟机遇到一条new指令时:1.先检查它的参数能否在常量池中定位一个符号的引用,同时检查该引用是否已经被加载、解析、初始化过,如果没有,则进行相应的类加载过程。2.类加载通过后,虚拟机为新生对象分派内存,一个对象所需要的内存大小在类加载时就完全确定,分派内存唱常使用两种方法A指针碰撞(Bump the point内存规整)和B空闲列表(Free List内存不规整)。同时,考虑到对象的创建是非常频繁的行为,为了保证更新操作的原子性,常用两种解决方法ACAS的失败重试以及B本地线程分配缓冲(Thread Loacl Allocation Buffer)。
对象在内存中的存储布局,分为3个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
- Header:包含两部分1.Mark Word(未锁定时:25bit的hashcode,4bit分代年龄,2bit锁标识,1bit固定为0),2.类型指针,即指向对象的类元数据的指针。(但,查找对象的元数据并不一定要经过对象本身,可以通过句柄和直接指针两种。另外,如果对象是个数组,还必须在对象头中记录数组长度,因为从元数据中无法得到数组大小。)
- 实例数据:默认分配策略中,相同宽度的字段总是分配在一起,在这个前提下,父类的字段在子类之前。
- 填充数据:由于虚拟机的自动内存管理系统要求对象的起始地址必须为8字节的倍数。
对象的访问方式
- 使用句柄。在堆中划分一块区域作为句柄池,栈中的ref引用的地址就是句柄地址,再从句柄池中得到对象的实例数据和元数据的地址信息。(该方式优点是ref中存储的是稳定的句柄地址,在对象被移动,尤其是gc时,只需要改变句柄中的实例数据指针。)
- 直接指针。在栈中的ref直接指向对象的地址,对象中包含了实例数据以及指向元数据的指针。(该方式优点是访问速度快,节省了一次指针定位的开销。)