/jvm

JVM 学习

Primary LanguageJava

###JVM 学习 2017年5月5日 19:40:01 根据《深入理解java虚拟机》、尚学堂架构师课程等。在学习完这些之后,还有一本百度阅读app上买的书,是根据go语言 从零开始编辑java虚拟机的。

####JVM内存区域

  • 线程隔离区(每个线程独立)

    1. 程序计数器

      较小的内存空间,可以看作当前线程所执行的字节码的行号指示器。通过改变这个计数器的值,选取下一条需要执行的 字节码指令。分支、循环、跳转、异常处理、线程回府都需要靠它完成。

      例如多线程的切换,是通过分配cpu时间片实现的,而切换后为了切换回来时当前线程能够恢复到正确的位置,就需要 靠它存储当前线程正在处理的字节码文件。

      如果线程执行的是java方法,这个计数器就保存正在执行的字节码指令地址;如果执行的是Native方法(本地方法, java底层的方法,通常用C实现),这个计数器就为空。

      这个区域是唯一一个在jvm规范只能没有规定任何OutOfMemoryError(内存溢出)情况的区域。

    2. 虚拟机栈

      生命周期和线程相同。它描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(方法运行时的基本数据结构), 用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成,就对应一个栈帧从入栈到出栈。

      java内存中常说,堆内存,栈内存。栈内存说的就是它,或者说是它存储局部变量的部分。

      操作数栈存放了计算过程的中间结果,同时作为计算过程中临时的存储空间。

      帧数据区,保持着访问常量池(方法区中的常量池)的指针。还有异常处理表。

      局部变量表存放了编译器可知的各种基本数据类型、对象引用(它不等同对象本身,是指向对象内存起始地址的指针,或者 指向代表对象句柄或其他与对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。(所以,java中的对象是没有 作用域的,只有对象的引用才有作用域)

      其中long和double占用2个局部变量空间,其余的占一个。局部变量中所需的内存空间是在编译时确认的,方法运行时,该 大小不会改变。

      如果线程请求的栈深度大于虚拟机允许的深度,就会抛出StackOverflowError(堆栈溢出)异常;如果虚拟机栈可以 动态扩展(大部分都可以),扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

    3. 本地方法栈

      与虚拟机栈相似,不过这个栈执行Native方法。异常也是一样抛出情况。

  • 共享区(所有线程共享)

    1. Java堆(GC堆)

      !!!注意,堆中可能会划分出多个线程私有的分配缓存区(TLAB)(就是volatile关键字中提到的那个导致线程不可见性的缓冲区)

      java堆在物理上可以是不连续的(内存中的不连续空间),只要在逻辑上连续就可以了。

      如果堆满了,抛出OutOfMemoryError异常。

      虚拟机启动时创建。所有对象实例和数组都在堆上分配(也不是绝对)。是垃圾回收的主要区域。分为新生代和老生代。 新生代分为eden(伊甸园)、s0(from)、s1(to)三个区域,s0,s1是两块大小相同并且可以互换角色的空间。 大多数情况下,对象会分配在eden区(如果对象过大,会直接进入老生代),经历过一次GC后,会进入s0或s1,之后没经历一次GC,年龄+1,当对象到达一定年龄后, 进入老生代(tenured区)。 关于s0和s1,GC的第X次处理,会处理s0,把s0中的垃圾对象处理了,然后把存活的对象拷贝到s1;第X+1次处理,就会处理s1, 把s1存活的对象拷贝到s0。如此往复。

    2. 方法区(永久代,PermGen ) !!!JDK8中被(元空间,metaspace) 取代;且一些杂项信息被移到堆中。

      存储已被虚拟机加载的类信息,常量,静态常量,即时编译后的代码等。

      运行时常量池。Class文件中除了有类的版本,字段,方法,接口等描述信息外。还有一个常量池,存放编译器生成的各种字面量和符号引用。

    3. 直接内存

      不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。

      NIO可以通过Native方法直接分配到直接内存,然后通过堆中的DirectByteBuffer对象作为这块内存的引用进行操作。可以提高性能, 因为避免了数据在java堆中好Native堆中来回复制。

      内存过大了,也会抛出OutOfMemoryError。


####虚拟机参数 在虚拟机运行过程中,虚拟机提供了一些跟踪系统状态的参数,是用给定的参数执行java虚拟机,就可以在运行时打印相关日志,用于 分析实际问题。对虚拟机的参数配置,主要就是对堆、虚拟机栈、方法区进行配置。

  • -Xloggc:d:/gc.log 打印gc日志
  • -XX:+PrintGC 遇到GC就会打印日志 +Print,+是使用 -是禁用
  • -XX:+UseSerialGC 配置串行回收期
  • -XX:+PrintGCDetails 可以查看详细信息,包括各个区的情况
  • -XX:+PrintCommandLineFlags:可以将隐式或者显示传给虚拟机的参数输出
  • -Xms 设置启动时初始堆大小 -Xmx20m 可以指定初始大小和最大堆大小相等,减少运行时的GC次数,从而提高性能
  • -Xmx 设置能获取的最大堆大小 -Xms5m
  • -Xmn 设置新生代大小,设置一个较大的新生代会减少老年代的大小,这参数对系统性能和GC有较大影响,一般 为整个堆空间的1/3到1/4。
  • -XX:SurvivorRatio:用来设置新生代中eden空间和from/to空间的比例。 例如: -XX:SurvivorRatio=eden/from=eden/to
  • -XX:NewRatio=x/y 设置老生代和新生代的比例 x表示老生代,y表示新生代
  • -XX:+HeapDumpOnOutOfMemoryError 在内存溢出时导出整个堆的信息
  • -XX:HeapDumpPath=d:/Test03.dump 设置导出堆信息后的存放路径 然后可以使用内存分析工具之类的进行分析
  • -Xss 设置虚拟机栈空间大小。也就决定了方法可以调用的栈最大深度
  • -XX:PermSize 设置方法区大小 !已经被废弃
  • -XX:MaxPermSize 设置方法区最大大小,默认为64M !已经被废弃 使用MaxMetaspaceSize
  • -XX:MaxTenuringThreshold 多少次GC后,对象进入老生代。默认为15
  • -XX:PretenureSizeThreshold 多少大小,可以直接进入老生代。但是如果值过小,对象会进入TLAB区域,最终还是进入新生代
  • -XX:-UseTLAB 禁用TLAB区域。
  • -XX:+TLABSize TLAB大小
  • -XX:PrintTALB 查看TLAB信息。
  • -XX:TLABRefillWasteFraction 设置维护进入TLAB空间的单个对象大小。是一个比例值。默认为64. 即如果对象大于整个空间的1/64,则在堆中创建。
  • -XX:ResizeTLAB 自调整TLABRefillWasteFraction阈值。

TLAB(thread local allocation buffer) 线程本地分配缓存。也就是volatile关键字中提到的缓冲区。每个线程都会有这样一个独立的空间。提高对线创建效率。 tomcat中在config/catalina.sh 设置jvm参数 应该尽量将对象预留在新生代,减少老生代的GC次数。

####GC(垃圾回收) #####垃圾收集算法

  1. 引用计数法:古老的。核心是在对象被其他锁引用时,计数器+1,引用失效则-1。会引发问题,无法处理循环引用的情况,还有就是 每次都进行加减,浪费系统性能。

  2. 标记清除法:第一个可以回收被循环引用的数据结构的垃圾回收算法。分为标记和清除两个阶段处理内存中的对象。 此算法,未被引用的对象不会被立刻回收,直到内存耗尽,程序被挂起,垃圾回收开始执行。 首先会标记可访问(被堆栈中的本地变量或静态变量引用的对象被称作可访问对象,如果对象是被另外的可访问对象中的域所引用, 就是间接访问对象,会被回收)的对象,然后回收未被标记的对象。会引发空间碎片问题。就是回收后的内存空间不是连续的, 非连续的空间工作效率。要低于连续的空间。也会周期性无响应,进行垃圾回收。

    所谓循环引用:A类,B类,A类中有B类属性,B类中有A类属性。然后创建a、b对象,a.b=b;b.a=a; a.b=null;b.a=null;

  3. 复制算法:(新生代中的from/to使用)将内存分为两块,每次只使用其中一块。GC时,将正在使用的内存中存留的对象复制到另一个未被使用的内存块中。 然后清除之前正在使用的内存中的所有对象。如此往复。

  4. 标记压缩法:(老生代使用)在标记清除法之上做了优化,把存活的对象会压缩到内存一端,而后进行垃圾清理。

    新生代和老生代的算法不同,是因为老生代的对象比较稳定,标记压缩就可以了;新生代的对象回收比较频繁。

  5. 分代算法:根据对象特点把内存分成n块,根据每个块的特点使用不同的算法。

    对于新生代和老生代来说,新生代回收频率高,每次回收耗时短;老年代回收频率低,耗时会长。 所以要尽量减少老生代的GC。

  6. 分区算法:将内存分成n个小的独立空间,每个小空间都可以独立使用。然后每次回收都针对某个小空间。 从而提高性能。 #####垃圾回收的停顿现象 为了让GC高效的执行,大部分情况下,GC时系统会进入一个停顿状态。停顿的目的是为了停止所有的线程。只有这样才不会有新的垃圾产生。 同时停顿是为了保证系统状态在某一瞬间的一致性,有益于更好地标记垃圾回收对象。 #####对象创建流程 1.尝试栈上分配(失败-->2) 基本类型? 2.尝试TLAB上分配(失败-->3) 3.尝试进入老生代(失败-->4) 4.分配到eden. #####垃圾收集器 1.串行垃圾回收器

使用单线程进行垃圾回收的回收器。对于并行计算能力弱的计算机,性能好。又分为新生代串行回收器和老生代串行回收器。

-XX:+UseSerialGC 使用该回收器 2.并行垃圾回收器

多个线程同时垃圾回收,对性能高的计算机,有较好表现。

  • (ParNew并行垃圾回收器)

它工作在新生代中。

-XX:+UseParNewGC 新生代使用该回收器,老生代则使用串行回收器

-XX:ParallelGCThread 指定线程数,一般与CPU相当。

  • (ParallelGC并行垃圾回收器)(JDK8默认垃圾回收器)

使用了复制算法。它的最大特点是非常关注系统吞吐量

-XX:MaxGCPauseMillis 设置最大垃圾回收的停顿时间。过小会导致GC频繁。

-XX:GCTimeRatio 设置吞吐量大小,一个1-100间的整数。默认情况取值99.那么系统将花费不超过1/(1+99)的时间用于GC。

-XX:UseAdaptiveSizePolicy 自适应模式。该模式下,新生代大小、eden、from/to的比例,以及晋升老年代的GC次数会被自动调整, 以达到在堆大小、吞吐量和停顿时间之间的平衡点。

  • (ParallelOldGC并行垃圾回收器)qps最高,比CMS高

老生代中使用的,也关注吞吐量,使用标记压缩算法。

-XX:+UseParallelOldGC 使用该回收器

-XX:+ParallelGCThread 设置垃圾收集时的线程数量

3.CMS回收器

全称 Concurrent Mark Sweep ,并发标记清除。主要关注系统停顿时间

-XX:+UseConcMarkSweepGC 设置该回收器

-XX:ConcGCThread 设置线程数量。

它不是独占的回收器。也就是垃圾回收中,系统仍在运行,仍有新垃圾产生。所以使用该回收器要确保足够内存可用。 它不会等到饱和时才去GC,而是在某一个阈值时开始。可以用-XX:CMSInitiatingOccupancyFraction 指定。默认为68. 也就是老生代空间使用率为68%时GC。

如果回收中,垃圾产生过快,会回收失败。虚拟机将启动老生代串行回收器进行回收,这会导致程序中断,直到垃圾回收完成。且停顿时间可能会很长。

-XX:CMSFullGCsBeforeCompaction 设置多少次CMS的GC后,对内存进行压缩,防止碎片。

4.G1回收器(jdk8中也不比ParallelGC快) JDK1.7中提出的。为了取代CMS.它属于分代回收,区分新生代和老生代。但它不要求整个eden区或者新生代、老生代是连续的, 它使用了分区算法。

-XX:+UseG1GC 使用该回收器 -XX:MaxGCPauseMillis 指定最大停顿时间。 -XX:parallelGCThread 设置并行回收的线程数量