多线程理解(二) JVM

一、JVM介绍:介绍线程内存模型之前,先介绍下JVM运行时的数据区是如何划分的。

Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区域,而绿色部分代表的是每个线程的私有数据区域。

1.Method Area方法区

方法区是被所有线程所共享的,所有的字段和方法的字节码,以及一些特殊方法如构造函数,接口代码也在此定义。所有定义的方法的信息都保存在方法区中,属于所有线程共享。

静态变量+常量+类信息+运行时常量池存在方法区中,实例变量存在堆内存中。

2.VM Stack 虚拟机栈

栈是跟随线程被创建而创建,随着线程被销毁而被销毁,所以对于栈来说不存在内存回收问题。栈是线程私有的,基本类型的变量和对象的引用变量都是在函数的栈内存中分配的。

栈的存储对象是,本地变量(输入参数和输出参数和方法内的变量),栈操作(出栈和进栈),栈帧数据(包括类文件、方法等)。

栈的运行原理是“先进后出,后进先出”。

3.Heap(堆)

堆是JVM内存中最大的区域,应用的对象和数据都存储在此区域中,堆也是线程共享的,也是GC(java垃圾回收)的主要区域。

堆内存主要分为三大区域

如图所示:新生代,老年代,永久代。

  • 新生代又分为Eden , Survivor两部分。同理,Survivor也分成S0 和S1两个生 存区,我们new出来的对象一般在Eden中分配内存,如果Eden内存满了, 就会进行垃圾回收(Minor GC),将存活的对象移至S0。S0和S1是两个大 小相等的区域,分配空间只会在一个中进行,另一个将辅助进行垃圾回收。 新生代的垃圾回收策略基于复制算法。其思想是将Eden区及两个Survivor 中的某个区,如S0区里面需要存活的对象复制到另外一个空的Survivor区, 如S1区,然后就可以回收Eden和S0区域里面的死亡对象。下一次回收就 对调S0和S1两个区的角色,S1用来存放存活对象而S0用来辅助回收垃圾, 如此循环利用。
  • 若S0或者S1区的存活对象把空间占满了之后,就把Survivor区中的存活对 象放入老年代。然后对Survivor区进行垃圾回收。
  • 并不是所有new出来的对象都是在Eden区中分配内存的,对于Serial和 ParNew垃圾收集器,通过指定-XX:PretenureSizeThreshold={size}来设置超过这 个阈值大小的对象直接进入老年代。
  • 如果老年代的空间也满了之后,就会进行Majar GC(Full GC),若进行Full GC 之后,仍然无法进行对象的保存,就会报OOM异常OutOfMemoryError。
  • 如果出现java.lang.OutOfMemoryError:Java heap space异常,说明Java虚拟机的堆内存不够,可能的原因有:
  1. Java虚拟机的堆内存设置不够,可以通过参数-Xms,-Xmx来调整(这两个参数下面会说到)
  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
  • 如果出现java.lang.OutOfMemoryError:PermGen space异常,说明Java虚拟机对永久代设置的内存不够,可能的原因有:
  1. 程序启动需要加载大量的第三方jar包。例如:在一个tomcat下部署太多的应用了。
  2. 大量动态反射生成的类不断被加载,最终导致perm区被占满。
  • 老年代:老年代主要保存新生代筛选出来的JAVA对象,一般池对象都在这 个区域活跃。
  • 永久代:永久代是一个常驻内存区域,用于存放JDK自身携带的Class, Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进 此区域的数据是不会被垃圾回收器收掉的,关闭JVM才会释放此区域所占用 的内存。

说明:

JDK1.6及之前:常量池分配在永久代。

JDK1.7:有,但已经逐步去永久代。

JDK1.8及之后:已经移除永久代,取而代之的是元空间(Metaspace)的区域。无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。

 

二、JVM如何判断垃圾回收的对象

  1. 引用计数算法

给每个对象添加一个引用计数器,每当有其他对象引用时,计数器加1,当引用结束时,计数器减1,如果一个对象的引用计数器为0,说明这个对象没有被引用,可以回收。

目前很多主流的虚拟机都已经抛弃了这种方法,因为这种方法没办法解决对象间循环相互引用的问题。

   2. 可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路称为“引用链”,当一个对象到“GC Roots”没有任何引用链相连时,则证明这个对象是不可达的。

说明:如果对象在进行可达性分析时,发现与“GC Roots”并没有任何引用链相连时,这个对象会被标记,并且经历第一次筛选。筛选的内容是:判断这个对象是否有必要执行finalizer()方法,如果对象没覆盖finalizer()方法或者虚拟机已经调用过finalizer()方法,则判断这个对象没有必要再执行(即可以直接回收)。

如果判断对象有必要执行finalizer()方法的话,则将这个对象放在一个叫F-Queue的队列中。并由一个虚拟机创建的,低优先级的Finalizer线程去执行它。这里的“执行”是指会触发这个方法,但不一定会等待它执行完。原因是如果一个对象在finalizer()方法中执行缓慢或者出现死循环将导致队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

Finalizer()方法是对象拯救自己的唯一机会,稍后GC将对F-Queue中的对象进行第二次标记时,如果对象在finalizer()方法中重新将自己与引用链上的某一个对象建立关联,将不会被回收,否则就会直接被回收。

 

三、JVM 垃圾回收算法

Jvm针对堆中不同代有不同的回收算法。针对新生代,主要采用复制算法,而针对老年代,通常采用标记-清除算法或者标记-整理算法来进行回收。

 

  1. 复制算法:

复制算法的思想是将内存分成相等的两部分,每次使用其中的一部分。当其中的一部分使用完之后,将还存活的对象复制到另一部分,然后将这一部分的内存进行回收。

复制算法简单而且高效,但是代价就是牺牲一半的内存用来进行复制。

新生代中98% 的对象都是朝生夕死,所以将内存分成一块较大的Eden区和两块较小的Survivor区。Hotspot默认的比例大小是8:1:1。

     2.标记-清除算法

标记-清除算法分成两阶段。

一是标记阶段:在标记阶段,首先标记需要回收的对象空间。

二是清除阶段:在清除阶段,将标记出来的对象空间回收。

标记-清除算法有两个问题:一是要经过标记和清除两阶段,效率不高;二是清除后空间可能产生碎片化问题。

     3.标记-整理算法

标记-整理算法有效的防止了标记-清除算法产生的碎片化问题。在标记需要回收的对象后,他将所有的存活对象挪到一起,然后再进行清理。

猜你喜欢

转载自blog.csdn.net/linjiaen20/article/details/81136518