结构与内存管理
目录:
- JVM运行时数据区
- 对象访问
- 垃圾回收算法
- 内存分配机制
- 内存回收
1、JVM运行时数据区
程序计数器
用来选取下一条需要执行的字节码指令、分支,来执行循环、跳转、异常处理、线程恢复等基础功能。
在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响、独立存储,我们称这类内存区域为“线程私有”的内存。
Java虚拟机栈
本地方法栈
Java堆
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。
当然垃圾回收的条件非常严格,当一个类在整个环境中不被引用的时候,这个类就可以卸载了
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方在反射访问该类的方法
运行时常量池(Runtime Constant Pool)是方法区 的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还会在编译器生成一些字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
java语言并不要求常量一定只能在编译器产生,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的intern() 方法。
直接内存
直接内存(DIrect Memory)并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,他可以Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存 的引用进行操作。这样在一些场景中显著提高性能,因为避免了再Java堆和Native堆中来回复制数据。
Object obj
这部分语义会反映到Java栈栈帧的本地变量表中,作为一个reference类型数据出现.而
new Object()
这局会反映到Java堆中,形成一块存储了object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机的实现的对象内存布局的不从,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查找到此类型数据的地址信息,这些类型数据则存储在方法区中。Java内存分配和回收的机制概括的说,就是分代分配,分代回收。对象将根据存活的时间被分为:
- 年轻代(Young Generation)。年轻代可以分为3个区域:Eden区和两个存活区(Survivor 0 、Survivor 1),一般比例是8:1:1
- 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快
- 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的)
- 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0
- 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效。
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。
-
年老代(Old Generation)
对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上
可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
-
永久代(Permanent Generation,也就是方法区)。
上面已经说了年轻代是停止-复制
算法。一般,老年代用的算法是标记-整理
算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了- XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置- XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。
转载自:https://www.jianshu.com/p/92e191188caf