白话JVM(二)内存区域

关于JVM的内存区域,按线程私有/共享分为:线程私有的栈,程序计数器,本地方法栈。线程共享的堆和方法区。其中方法区在1.8之后叫做元空间。
先从简单的说起吧,栈时每个线程都有的,比如,main方法就会起一个主线程栈,或者,用Java提供的API,也会创建一个线程栈。线程栈内保存基本数据类型的变量和自定对象的引用而非实例,当然也有可能是逃逸分析后的实例。线程栈中还有更复杂的结构,他就是栈帧,每一个方法对应一个栈帧。总得来说线程栈是一个FIFO先入先出的结构,每当有线程调用一个方法时,就会压入一个栈帧,执行完的方法的栈帧直接弹出,最底层的是main方法的栈帧,此处可能会出现stackOverFlow,即压入方法的栈帧太多,栈区的内存不够分配。同时,线程栈的大小是可以调的,但是能调的的是线程栈的大小,而非栈区,当线程栈调得越大时,能压入的栈帧越多,但是能起的线程越少。再来说说栈帧的内部吧,他也是有复杂的结构的,有局部变量表,操作数栈,动态链接,方法出口。其中局部变量表和操作数栈需要结合JVM指令码来说,比如方法中int a=1,就会在局部变量表中为a分配一块内存,在将a压入操作数栈,然后将1赋值给a。可以看出,操作数栈就是操作数中的临时内存。还有就是局部变量会有一个默认的内存来存放this指针。对于方法出口,他相当于方法的程序计数器,此方法中调用的子方法执行完后,就会回到方法出口,继续执行。
再来说说和栈区相似的本地方法栈,本地方法栈中存放的是Java调用native方法时,native方法产生的临时变量。
再来说说程序计数器,他也是一块内存,用来存放的是线程马上要执行的JVM指令码的行号,而且JVM会将行号和内存地址进行映射。显然程序计数器也是线程私有的。程序器是被字节码执行引擎操作的,每执行一条JVM指令码,字节码执行引擎将其加一。
既然提到了执行引擎,那就还谈谈他吧,他有两个作用,一个是操作程序计数器,一个是执行被类加载子系统装入到方法区的类元信息。
说完简单的,再来说说复杂的方法区和堆。其中方法区在1.8之前叫永久代,但是1.8后改为元空间。所谓的方法区相当于JVM层面定义的一个抽象概念,而永久代和元空间是这个抽象概念的具体实现。他用来装载被类装载子系统加载到内存区域的信息,也叫类元信息。所谓的类元信息,就是类的相关的一切信息,包括类名称,方法名,返回值,局部变量,静态变量,他们被加载解析到元空间后就叫类元信息。而类元信息是怎样被找到的呢?堆中对象实例的对象头中有能指向方法区中的类元信息,所以实例可以通过getClass方法得到类对象。同样的,方法区中有时也有堆中对象的引用,如,当在全局new一个static静态对象时,对象的引用也会放到方法区中。同样的,栈帧中new时,栈帧也会保存堆中的引用。当类被加载到方法区时,有一个特殊过程叫静态链接。类似的还有动态链接,他们都是将符号引用替换成直接引用,所谓的直接引用是指方法区对应的指令码的内存地址。不同的是一个发生在类加载过程,一个发生在程序运行时完成。静态链接只处理静态方法,main方法,其他的方法需要交给动态链接。所谓的符号引用是包括代码中的方法名,属性名。方法区在1.8时发生了重大的变化,在1.8之前,方法区是由永久代实现,而1.8之后,方法区的实现叫做元空间,并且使用的是直接内存,并不占用JVM内存。
最后来说内存中最大的一块,他叫做堆,堆分为年轻代和老年代,其中的年轻代又分为Eden区和Survivior区。而Survivor区又分为From区和To区,若不设置参数,则年轻代各区域的内存大小比为8:1:1。年轻代和老年代的内存大小比为2:1。new出来的对方放在Eden区,但大对象除外和逃逸分析除外,大对象可能出生在老年代。当Eden区满了之后,会触发MinorGC。所谓的MinorGC又叫做YoungGC,他会对年轻代的Eden区和Survivior区都进行回收,回收的是垃圾对象,也就是无引用对象,所谓的无引用对象,是指栈帧方法执行new对象时,栈帧会保存堆中对象的引用,当方法执行完后,栈帧的局部变量消失,引用消失,有可能堆中就没有引用指向他了,此时就被叫做无引用对象,关于检索无引用对象也是有特定方法的GCRoot根,和引用计数器,就在后面说吧。当第一次触发MinorGC时,Eden区存活的对象会进入Survivor区中的From区,当From区满时,再次触发MinorGC,会将Eden区和From区的存活对象进入To区,此时Eden区和From区就被情况,又可以在Eden区new对象了。也就是说,每当触发MinorGC,就会使得Eden区和Survivor区中的一半再经历了回收后的存活对象会进入到Survivor区中的另一半区域中。而没当经历了一次MinorGC,堆中对象的对象头中的MarkWord中的age字段就会加1,默认情况下当age字段达到15时,对象就会进入老年代,当然这个参数在调优的时候是要设置的,根据情况来设置小一点,减少没必要的移动操作。进入老年代的一般是Spring容器中的bean,缓冲池之类的生命周期和应用程序相同的那类对象。而当老年代满了之后,则会进行FullGC,所谓的FullGC也是一种垃圾回收,他和MinorGC的区别是,MinorGC只收集年轻代,而FullGC需要收集整个堆内存,甚至还有方法区中的静态变量,因为回收范围广,所以很显然,他的耗时长。所以调优的时候就是尽量使用MinorGC,而避免触发FullGC。FullGC和MinorGC也有相同的地方,那就是他们都会STW,也就是停止应用程序的线程,专心执行垃圾收集线程,他当然会带来用户体验的不好,但是考虑到并发执行垃圾回收的话,会造成垃圾回收不干净和垃圾回收太干净而导致的空指针异常,所以Oracl权衡之后,最终还是用了单线程STW。但也有很大的优化空间,比如之后的CMS垃圾收集器和G1收集器就会适当得进行并行垃圾收集,来提高用户体验。
有一点需要说明的是,之前提到对象都new在堆区,是不对的,因为JVM有个默认开启的逃逸分析优化,想知道逃逸分析首先要明白他发生在什么时期,他发生在即使编译JIT时,所谓的即使编译是指JVM的编译模式,他是解释模式和编译模式的组合,只会一次性编译热点代码,将其缓存,而非一次性编译所有代码,因为编译后的机器码是编译前的JVM指令码的十倍以上,都编译的话会非常消耗内存,所以不可能一次性全部编译。而在JIT中会进行包括逃逸分析在内的三种优化,比如代码中声明了却没有使用的局部变量会被忽略掉,不进行编译,再就是编译器级别的指令重排序也是发生在此时,更重要的就是逃逸分析了,逃逸分析会分析局部变量的动态作用域,当他只在方法内使用,而没有作用于方法外,比如传参,赋值给全局变量,那么他就不会维护在堆中,而是维护在线程栈中,这样做的好处也是为了减少GC的次数,因为他节约了堆的空间。但也不是绝对放在栈中,当这种对象太大,栈帧放不下时就会还是交给堆来维护。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值