Java虚拟机运行时数据区域
Java虚拟机运行时数据区域就是Java虚拟机管理的内存区域。这个数据区域分为方法区、堆、虚拟机栈、本地方法栈、程序计数器
程序计数器(线程私有)
对于Java程序计数器来说,如果执行Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行Native方法,那么计数器的值为空(Undefined)
虚拟机栈(线程私有)
每个Java方法在执行的时候都会创建一个Stack Frame,这个Stack Frame用来存储局部变量表、操作数栈、动态链接、方法出口等。每个方法执行过程都是一个Stack Frame在虚拟机栈入栈到出栈的过程。其中局部变量表保存了各种基本类型和对象引用以及方法的返回地址(即字节码指令地址)。这个局部变量表在编译期间就完全确定了,在方法运行期间是不会改变的。如果当线程请求的栈深度大于虚拟机栈允许的深度,那么抛出StackOverflowError,当虚拟机栈动态拓展时无法申请到足够的空间,那么抛出OutOfMemoryError。
本地方法栈
与虚拟机栈类似 堆堆是Java虚拟机所管理的内存中最大的一块,被所有线程共享,它是在虚拟机启动的时候创建的。堆就是用来创建对象实例和数组的。堆一边被分为新生代和老年代,其中新生代还可以被分为Eden空间、From Survivor空间和To Survivor空间。
方法区
又名永久带,这只是Hotspot的一个处理,他们用永久带来实现方法区。用来存储虚拟机加载的类、常量、静态变量、即时编译器编译后的代码。其中方法去里面还包括了运行时常量池,因为一个Class文件里除了类的版本、字段、方法、接口等描述信息之外还有很多编译期间产生的各种字面量和符号引用,而这些都会在运行时常量池里面保存。当然,在运行期间产生的新的常量池也会被放到池里面去。String的intern方法就是利用这个特性。
对象的创建过程
1、碰到new的时候,首先检查new这个指令里面的参数是否能在常量池里面定位到,并且坚持这个符号引用代表的类是否被加载、解析和初始化过。如果没有,需要先执行相应地类加载过程
2、类加载检查通过之后,开始为新生对象分配内存。对象所需的内存大小在类加载完之后即可确定了。分配对象的过程就是将一块确定大小的内存从Java堆里面划分出来。加入Java堆中的内存是规整的,即所有内存都是放到一边的,空闲的在另一边,中间有指针作为分界点。那么这种分配内存的过程就是指针移动相应内存大小的距离,这种内存分配方式就是“指针碰撞”;而另外一种分配方式即在内存不规整的前提下进行的,这是虚拟机需要维持一个空闲内存记录表,每次要分配新内存的时候在表里面进行查找。
3、考虑每次分配内存的时候存在多个线程竞争,那么就需要使用TLAB这种解决方案
4、分配完之后,需要进行空间初始化,这样可以保证对象的实例字段可以不赋值即可使用
5、对象进行相应地设置,比如对象属于哪个类,如何找到类元数据信息、对象的hashcode、对象的GC分代年龄。这些都保存在对象头里面。
6、最后执行<init>方法
对象的内存布局
三个部分:对象头(Header)、示例数据(Instance Data)、对齐填充(Padding)
内存的访问定位
两种方式:句柄和直接。其中句柄方式就是在堆里面维持一个句柄池,然后当通过栈里面的地址先找到句柄,句柄包含了存在堆里的对象实例数据和存在方法区的对象类型数据的地址。这样做的好处比较灵活,当因为垃圾回收等原因造成对象实例移动,只需改变句柄即可;另一种方式,直接访问,即栈里面的地址直接访问堆里面的对象实例,其中对象实例里面还包括的了方法区里面的对象类型数据的地址。
垃圾回收以及内存分配
三个目的:哪些内存需要回收、什么时候回收、如何回收哪些内存需要回收也就是如何判断需要回收的内存。
a、引用计数法
就是通过给对象添加引用计数,当有引用它的时候,计数加1,引用失效时,减一,但是由于存在相互引用,没被采用。
b、可达性
通过从一个GC Roots的起点开始搜索可达对象,不可达对象即可被判断为可回收对象。其中GC Roots包括这些:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象
引用的分类
强引用(默认就是)、软引用(在发生内存溢出之前,这些对象会被列入第二次回收的范围,如果这次还没回收足够的内存,抛出异常)、弱引用(当垃圾回收器进行工作的时候,无论当前内存释放足够,被弱引用关联的对象都会被回收)、虚引用(在对象被回收的时候收到系统通知)
对象回收的再判定
在经历过可达性分析之后呢,其实只是给这个要回收的对象判了一个缓刑。当这个对象没有覆盖finalize()或者虚拟机已经调用过它的finalize方法,那么它就被视为没必要执行。如果对象被视为有必要执行垃圾回收,它会被放到F-Queue队列里面,那么它接下来会被一个FInalizer的线程去执行。GC这时会对这个队列进行第二次标记,如果这个时候对象没有在finalize方法里重新引用自己的话,那么就会被真的回收了。
方法区的垃圾回收(也就是永久带的垃圾回收)
这里面主要是对废弃的常量和无用类的回收。无用类的判断就是三个条件:该类所有实例被回收、加载该类的ClassLoader被回收、该类对应的Class对象没有任何地方被引用,无法通过任何地方被反射访问该类的方法。
什么时候回收
在内存空间不足的时候,不能够分配新的对象的时候。如果发生在新生代,那么就是新生代GC,即MinorGC,发生在老年代,那就是Major GC。
如何回收(即垃圾回收算法)
a、标记-清除
两个阶段。首先标记所有要回收的对象,然后统一回收。
b、标记-复制(新生代)
把内存分为两块大小相等的,然后当一块用完的时候,将存活的对象复制到另一块上面去,然后把已经使用过的清理掉。后来这样由于导致内存使用率不高,因为IBM发现新生代的对象98%的都是很快就死掉了,因此后来就演变成将内存分为较大的Eden空间和两块较小的Survivor空间,Eden和Survivor的比例为8:1。每次使用Eden和其中一块Survivor空间,当内存不够用时,会将Eden和Survivor中还存活的对象(大部分都不到10%)复制到另一块Survivor空间,然后再将之前那的Eden和Survivor空间清除。当然还有一些意外的情况,就是万一之前存活的对象超过了10%,那么就会使用分配担保机制进入老年代。
c、标记-整理(老年代)
类似于标记-清理,只不过后来是通过将移动所有存活的对象,然后清理相应地端边界以外的内存。
d、分代收集
内存分配
对象优先在Eden分配大对象直接进入老年代长期存活对象进入老年代(每经历过一次Minor GC,对象年龄加一。默认年龄的阈值是15,即这之后就要进入老年代了。)