Skip to content

Latest commit

 

History

History
118 lines (74 loc) · 10.5 KB

JVM笔记.md

File metadata and controls

118 lines (74 loc) · 10.5 KB

JVM 笔记

  • Class Loader 类加载器
    • 负责加载 class 文件,class 文件在文件开头有特定的文件标识,并且 classloader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
  • Native Interface(Java 中会有 native 关键字标识的方法)
    • 本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 c/c++ 程序,Java 诞生的时候,是 c/c++ 横行的时候,要想立足,必须可以调用 c/c++ 程序,于是就在内存中专门开辟了一块区域处理标记为 native 的代码,它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载 native libraies 。
    • 目前该方法的使用越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机,或者 Java 系统管理生产设备,在企业级应用中已经比较少见。因为现在异构领域的通信很发达,比如可以使用 socket 通信也可以使用 webservice 等等。
  • Method Area 方法区
    • 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间
    • 静态变量、常量、类信息、常量池都存在方法区中
  • PC Register 程序计数器
    • 每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
  • Native Method Stack 本地方法栈
    • 它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载 native libraies

JVM 中栈和堆

栈管运行,堆管存储

Stack 栈是什么

栈也叫栈内存,主管 Java 程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放。对于栈来说不存在垃圾回收问题,只要线程一结束该栈就 over,生命周期和线程一致,是线程私有的。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。

栈存储什么

栈帧中主要保存 3 类数据:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量
  • 栈操作(Operand Stack):记录出栈、入栈操作
  • 栈帧数据(Frame Data):包括类文件、方法等

栈运行原理

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(method)和运行期数据的数据集,当一个方法A 被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了 B方法,于是产生栈帧F2,也被压入栈中,B方法又调用了 C方法,于是产生栈帧F3,也被压入栈。执行完毕后,先弹出 F3栈帧,再弹出F2栈帧,再弹出 F1栈帧。遵循 「先进后出」的原则。

堆内存示意图

新生区:

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden Space)和幸存区(Survivor Space),所有的类都是在伊甸区被 new 出来的。幸存区有两个:0区(Survivor 0 Space)和 1区(Survivor 1 Space)。当伊甸园的空间用完时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园中不再被其它对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这时候将产生 Major GC(Full GC),进行养老区的内存清理。若养老区进行了 Full GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常

如果出现 java.lang.OutOfMemoryError:Java heap Space 异常,说明 Java 虚拟机的堆内存不够。原因有二:

  • Java 虚拟机的堆内存设置不够,可以通过参数 -Xms -Xmx 参数来调整
  • 代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集(存在被引用)

养老区:

养老区用于保存从新生区筛选出来的 Java 对象,一般池对象都在这个区域活跃。

永久区:

永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class ,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

如果出现 java.lang.OutOfMemoryError:PermGen Space ,说明是 Java 虚拟机对永久代 Perm 内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方 jar 包。例如:在一个 tomcat 下面部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。

  • JDK1.6 及以前:有永久代,常量池1.6在方法区
  • JDK1.7 :有永久代,但已经逐步 "去永久代",常量池1.7在堆
  • JDK1.8 及以后:无永久代,常量池1.8在元空间

内存划分

实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息、普通常量、静态常量、编译器编译后的代码等,虽然 JVM 规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。

对于 HotSpot 虚拟机,很多开发者习惯将方法区称之为 "永久代",但严格本质上说两者不同,或者说使用永久代来实现方法区而已。永久代是方法区的一个实现。jdk1.7版本中,已经将原本放在永久代的字符串常量池移走。

常量池是方法区的一部分, Class 文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。

GC算法

概述:

JVM 在进行 GC 时,并非每次都对新生代、老年代、永久代三个内存区域一起回收,大部分回收的都是指新生代。因此 GC 按照回收的区域又分了两种类型,一种是普通 GC(minor GC),一种是全局 GC(major GC or Full GC)

  • 普通 GC(minor GC):只针对新生代的 GC
  • 全局 GC(major GC or Full GC):针对老年代的 GC,偶尔伴随着对新生代的 GC 和对永久代的 GC

复制算法:

年轻代使用的是 Minor GC ,这种 GC 算法采用的是复制算法(Copying)

复制算法原理:

Minor GC 会把 Eden 中的所有活的对象都移到 Survivor 区域中,如果 Survivior 区中放不下,那么剩下的活的对象就被移到 Old generation 中,也即一旦收集后,Eden 就变成空的了。

当对象在 Eden(包括一个 survivor 区域,这里假设是 from 区域)出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象),则使用复制算法,将这些仍然还存活的对象复制到另外一块 Survivor 区域(即 to 区域)中,然后清理所使用过的 Eden 以及 Survivor区域(即 from 区域),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC ,就将对象的年龄 +1 ,当对象的年龄达到某个值时(默认是 15 岁),这些对象就会成为老年代。

复制算法解释:

年轻代中的 GC 主要使用的是复制算法(Coping)

HotSpot JVM 把年轻代分为了三个部分: 1个Eden区和 2 个 Survivor 区(分别叫 from 和 to)。默认比例为 8:1:1 。一般情况下,新创建的对象都会被分配到 Eden 区,这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC ,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到老年代中。因为年轻代中的对象基本都是朝生夕死,所以在年轻代的垃圾回收算法使用的是复制算法。复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上去。复制算法不会产生内存碎片。

在 GC 开始的时候,对象只会存在 Eden 区和名为 "From" 的 Survivor 区,Survivor 区 "To" 是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到 "To",而在 "From" 区域中,扔存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值)的对象会被移动到老年代中,没有达到阈值的对象会被复制到 "To" 区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候 From 和 To 会交换他们的角色,也就是新的 To 就是上次 GC 前的 From ,新的 From 就是上次 GC 前的 To。不管怎么样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复遮掩搞得过程,知道 To 区域被填满,To 区域被填满后,会将对象移动到老年代。

因为 Eden 区对象一般存活率较低,一般的,使用两块 10% 的内存作为空闲和活动区间,而另外 80% 的内存,则是用来给新建对象分配内存的。一旦发生 GC,将 10% 的活动区间与另外 80%中存活的对象转移到 10% 的空闲区间,接下来,将之前 90% 的内存全部释放,以此类推。

复制算法缺点:

  • 浪费内存,始终有内存空闲
  • 如果对象存活率很高,假设是 100%,那么我们就要将所有对象都复制一遍,并将所有引用地址复制一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。所以复制算法想要使用,最起码对象的存活率要非常低才行。