Java虚拟机具有自动内存管理机制,我们可以显示地创建对象,由虚拟机为我们free对象。但是,一旦出现内存泄露、溢出等问题,就需要我们熟悉内存区域,才可以有效的解决。
1.程序计数器:
它实际上是一块很小的内存空间,用来指示当前线程此刻所执行的字节码的行号。它的作用有两种:①JVM是通过改变程序计数器的值来选取下一条要执行的字节码指令的。在线程切换时,记录当前线程执行的字节码行号,用于切换回正确的位置。因为Java虚拟机的多线程执行实际是通过多个线程轮转切换来实现的,所以每个线程都必须私有一个程序计数器,用于记录它被切换时执行到的位置。
2.虚拟机栈:
我认为 虚拟机栈是用来描述线程的,当开辟一个线程时,JVM会为其分配一块私有的栈空间;而虚拟机栈中的栈帧是用来描述线程中的方法的,在此栈对应的线程中执行一个方法时,JVM会为这个方法分配一个栈帧,方法的开始执行到结束就对应着栈帧的入栈与出栈。栈帧中包括局部变量表、操作数栈、动态链接与方法出口。
局部变量表:存放方法的形参与局部变量。(补充:局部变量包括基本数据类型、对象引用等,它们在局部变量表中以槽的形式存在。只有long与double占两个槽位,其他都只占一个)。当方法被调用时,JVM通过局部变量表来完成方法实参到形参的传递。栈帧在编译时期就已经完成了内存分配,即运行期间局部变量表的大小不会受到变量大小的影响。(补充:编译的结果是把机器码转变为字节码)。
操作数栈:存放方法执行时用到的数据类型与操作指令。这些数据类型在方法调用时入栈并完成数据的运算
如何完成数据运算?JVM编译时期进行词法分析和语法分析,生成抽象语法树,再遍历语法树生成线性的字节码执行流,即可以执行的指令集。
i++是怎么解析成字节码的:先由局部变量表压入操作数栈,执行incc叠加操作,栈顶出栈并压入局部变量表中。 动态连接:在编译时期指向运行时常量池中的该方法的符号引用,在方法执行时被转换为直接引用。(补充:在Class文件常量池中的另一部分符号引用在类加载时被转化为直接引用,称为静态解析)
方法出口:存放当前方法在主调方法中被调用的位置信息(计数器中的某个数值)。方法正常调用完成时,程序会根据方法出口的值调整PC计数器,指向主调方法后面正确的指令(补充:同时可能会把返回值压入操作数栈中);方法异常退出时,一般需要通过栈外的异常处理器表完成。 在栈中分配内存是要比堆块的,因为每个线程都有独立的栈,所以分配内存时不需要加锁保护。所以new对象时,编译器如果认为在栈中分配不影响功能语义时,会自动改为在栈中分配。 3.本地方法栈
为栈中的本地方法服务。
4.Java堆:
Java堆就是虚拟机中用于存放对象实例的内存区域。它在物理空间上不一定是连续存储的,但在逻辑上我们认为它连续存储。其实Java堆本质上是没有分区的,只是由于垃圾回收器的回收共性,使得我们认为Java堆具有年轻代、老年代这样的分区,我们可以把它理解成一种设计风格。而且最新的G1垃圾回收器就没有采用分代设计了嘛。
所以我站在垃圾回收器的角度,来说一下我理解的年轻代与老年代:
年轻代与老年代实际上是基于分代收集理论来设计的:垃圾收集器进行回收时会根据对象的年龄,把它们集中分配到Java堆中的不同区域进行存储,这样每次垃圾回收就可以根据不同的区域来选择不同的关注点,从而选择适合它的垃圾回收算法。比如说,垃圾收集器把存活时间短的对象集中放在年轻代,那么对于年轻代 垃圾收集器只需要关注如何保留少量的存活对象,而不需要关注如何清除大量要被回收的对象,即使用标记-复制算法;垃圾收集器把存活时间长的对象集中放在老年代,那么对于老年代,垃圾收集器只需要关注如何回收少量垃圾对象,而不需要关注如何保留大量存活对象,即使用标记-整理算法。这样的设计风格优化了垃圾收集器的回收成本和内存的空间利用率。(补充:进行年轻代回收时,我们不需要为了少量的跨区域引用而去扫描整个老年代。即存在相互引用的两个对象应该倾向于同时生存与消亡。)
大对象为什么直接被放入老年代呢?
大对象是指需要大量连续内存空间的对象嘛。如果新创建的大对象直接进入年轻代,很有可能出现明明还有很多小空间但无法存放大对象而导致频繁地进行垃圾回收。而且年轻代垃圾回收使用标记-复制算法,所以在两个元区之间复制大对象会出现高额的内存复制开销。所以我们把大对象直接放入老年代。
对象什么时候被放入老年代?
①对象首先在年轻代的Eden元区诞生,熬过一次Minor GC后会被转移到Survivor元区,对象头中的分代年龄会被记为1。之后每熬过一次Minor GC分带年龄就会加一。当分带年龄达到一定程度时(默认15,因为HotSpot在 对象头中的标记字段里记录年龄,可分配的空间只有4位,所以最多可以记录到15),对象就会被移动到老年代。
②动态对象年龄判断:若survivor元区中小于某个年龄的对象之和大于survivor元区大小的一半,则大于该年龄的对象会直接进入老年代。
年轻代为什么要有Survivor元区呢?
年轻代的垃圾收集基本都是使用标记-复制算法,所以Eden元区和Survivor元区也是基于标记-复制算法来说的。Java堆中年轻代的对象有98%熬不过第一轮GC,所以我们根据标记-复制算法的特性,把新生代分成了一块Eden元区和两块Survivor元区,大小比例为8:1:1。在发生垃圾回收时,会将Eden元区和Servivor元区中依然存活的对象一次性复制到另一个Survivor元区中,然后直接清理用过的空间。如果没有Survivor元区,则Eden元区每一次Minor GC都会把所有的存活对象都移入老年代,很快就触发full GC。Survivor元区保证了只有经过一定次数Minor GC还没被回收的对象才会被移入老年代,减少了full GC的发生。
年轻代中是有两个Survivor元区的。我们假设只有一个Survivor元区,则第二次Minor GC时,Survivor元区与Eden元区中都有存活对象。那么把Eden元区中的存活对象硬放到这个Survivor元区时,存活对象的内存是不连续的。如此重复就会导致产生大量的内存碎片,导致Minor GC触发频繁。然而有两个Survivor元区时,我们就可以使用复制算法,把存活对象复制到其中一个Survivor元区中,然后清除Eden元区和另一个Survivor元区,再交换角色重复进行。这样就保证了存活对象在Survivor元区中占有连续的内存空间,提高内存的空间利用率。
5.方法区:
它在JDK1.7之前存储被虚拟机加载的类型信息(基本上是class文件)、常量池与静态变量,而JDK1.7及其之后,运行时常量池和静态变量被移到了Java堆中,方法区只存放class文件。
运行时常量池存放被虚拟机加载的各种字面量和符号引用。符号引用是能够正确定位到目标的字面量,它定位的内容不一定已经加载在虚拟机中。直接引用是直接指向目标地址的字面量(补充:在运行过程中的新常量也可以放入常量池中,比如String.intern())
- 对于String类型:JDK1.6时String类型全部存在方法区中的String常量池中,JDK1.7时String类型全部存在堆中的String常量池中,JDK1.8时String类型:字面量对象存在于堆中的String常量池中,new出的对象存在堆中。
- 对于Byte、Integer、Short、Long、Charater类型,只有数值小于127时才有常量池。而且池中默认有[-127,127]范围的缓存数据,可以直接被使用。(Character是[0,127])
6.直接内存:(这个我想在Netty中写)
然鹅,除了程序计数器,其他区域在程序运行过程中都有可能出现OOM ^^^---^^^转接OOM。