JVM深入理解(二)-结构与内存管理

结构与内存管理

  目录:

  1.  JVM运行时数据区
  2.  对象访问
  3.  垃圾回收算法
  4.  内存分配机制
  5.  内存回收

 

  1、JVM运行时数据区

  

   程序计数器

  用来选取下一条需要执行的字节码指令、分支,来执行循环、跳转、异常处理、线程恢复等基础功能。

  在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响、独立存储,我们称这类内存区域为“线程私有”的内存。

  Java虚拟机栈

  Java虚拟机栈(Java Vitual Machine Stacks)也是线程私有的,它的生命周期与线程相同。线程中每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。

  本地方法栈

  本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用非常类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

  Java堆  

  Java堆(java heap)是Java虚拟机所管理的内存中最大的一块。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。同时堆是按代分配的,老年代、新生代(Eden区 2个、Survivor区),垃圾回收会讲到。
 
   方法区

  方法区(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堆中来回复制数据。

 

  2、对象访问
  Object obj = new Object();
  
  在这句代码上,Object obj这部分语义会反映到Java栈栈帧的本地变量表中,作为一个reference类型数据出现.
new Object()这局会反映到Java堆中,形成一块存储了object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机的实现的对象内存布局的不从,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查找到此类型数据的地址信息,这些类型数据则存储在方法区中。
  主流的访问方式有两种个: 使用句柄  和  直接指针
  
  句柄式访问:
  java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
  
  
  指针访问方式:
  reference变量中直接存储的就是对象的地址,而Java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。
  
  
  这两种访问对象的方式各有优势,使用句柄访问方式最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式
 
  3、垃圾回收算法
  标记-清楚算法问题。存在效率问题,还会产生大量不连续的内存碎片。
  复制算法。将可用内存按容量划分为 大小相等的两块,每次只是用其中的一块。当一块内存用完了,就将还存活着的对象复制到另外一块上面,然后把已经使用过的内存空间清理掉。
  
  标记-整理算法。标记过程和“标记-清楚”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
  
 
  4、内存分配机制

  Java内存分配和回收的机制概括的说,就是分代分配,分代回收。对象将根据存活的时间被分为:

  • 年轻代(Young Generation)。年轻代可以分为3个区域:Eden区和两个存活区(Survivor 0 、Survivor 1),一般比例是8:1:1
    1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快
    2. 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的)
    3. 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0
    4. 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
    5. 当两个存活区切换了几次(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,也就是方法区)。

 

  5、内存回收

  上面已经说了年轻代是停止-复制算法。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。

  在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了-    XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-  XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)

  永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。

 

转载自:https://www.jianshu.com/p/92e191188caf

posted @ 2021-08-11 15:01  tuanz  阅读(42)  评论(0编辑  收藏  举报