深入分析JavaWeb内幕读书笔记——(四)

JVM内存结构

JVM按照运行时数据存储结构来划分内存结构,将其划分成几种不同格式的数据分别存储在不同区域,他们统一称为运行时数据(Runtime Data)。其中包含程序本身的数据信息和JVM运行程序需要的额外数据信息(例如记录当前程序指令执行的指针)等。
Java虚拟机规范将Java运行时数据划分为以下6种:

  • PC寄存器数据
    PC寄存器用于保存当前正常执行的程序的内存地址;
  • Java栈
    当创建一个线程时,JVM就会为其创建一个对应的Java栈,在这个栈中会含有多个栈帧,栈帧与每个方法关联起来,每运行一个方法就创建一个栈帧,栈帧中会含有内部变量(在方法内定义的变量)、操作栈和方法返回值等信息。
    当方法执行完成时栈帧会弹出栈帧元素作为方法的返回值,并清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈(即当前正在执行的方法),PC寄存器也会指向这个地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当这个栈帧中调用另一个方法时,会创建一个新的栈帧来和它对应,新创建的栈帧又被放到Java栈的顶部,变成当前活动栈。当这个栈帧中的所有指令执行完成后,这个栈帧被移出Java栈,原来调用的这个方法的栈帧再次变为活动栈,并且被移出Java栈的返回值变为这个栈帧的操作栈中的一个操作数。如果没有返回值,则当前栈帧的操作栈的操作数无变化。

  • 堆是存储Java对象的地方,每一个存储在堆中的Java对象都会是这个对象的类的一个副本,它会复制包括继承自父类的所有 非静态 属性。堆被所有Java线程共享,所以对它的访问要特别注意同步问题,方法和对应的属性都要保证一致性。
  • 方法区
    方法区是存储类结构信息的地方,class文件被加载到JVM时,JVM会将从其中解析出可识别的几个部分存储在不同的数据结构中,其中的常量池、域、方法数据、方法体、构造函数,包括类中的专用方法、实例初始化、接口初始化都存储在这个区域。
  • 本地方法区(本地方法栈)
    本地方法栈是为JVM运行Native方法准备的空间,它的作用与Java栈类似,由于很多Native方法时通过C语言实现的,通常它又叫C栈,JVM利用JIT技术是会将一些Java方法重新编译为Native Code代码,这些代码也利用这个栈来跟踪方法执行状态。
  • 运行时常量池(Runtime Constant Pool)
    它是方法区的一部分,它的存储也受方法区的规范约束,如果它无法分配,同样会抛出OutOfMemoryError。它代表运行时每个class文件中的常量表。包括编译器的数字常量、方法或域的引用(在运行时解析)。Runtime Constant Pool的功能类似于传统编程语言的符号表。每个Runtime Constant Pool都是在JVM的方法区中分配的,每个Class或者Interface的Constant Pool都是在JVM创建class或接口时创建的。

JVM内存分配策略

  • 通常的操作系统的内存分配策略:

    • 静态内存分配
      静态内存分配时指在程序编译时就能确定每个数据在运行时的存储空间需求,因此在编译时就可以为其分配固定的内存空间。这种策略不允许程序代码中含有可变数据结构,也不允许嵌套或递归的出现,因为他们会导致编译程序无法计算准确的存储空间需求
    • 栈内存分配
      栈内存分配也称为动态存储分配,是由一个类似堆栈的运行栈来实现的。在栈内存方案中,程序对数据区的需求只有到运行时才能知道,但是规定运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小,这样才能为其分配内存。栈式内存分配按照先进后出的原则进行分配
    • 堆内存分配
      堆内存在程序运行时才执行,它的运行效率比较差,但自由度是最高的
  • Java中的内存分配

    栈随着线程的创建而创建,一个线程的方法的调用和返回对应这个栈的入栈出栈操作。线程激活一个Java方法时,就会向栈中压入一个栈帧,这个栈帧即是当前栈帧,在此方法执行期间,这个栈帧将保存方法参数、局部变量、中间计算过程和其他数据。

    栈中主要存放一些基本的变量数据(int、short、long、byte、float、double、boolean、char)和对象句柄(引用类型)。它的存取速度比堆块,仅次于寄存器,栈数据可以共享。但其缺点是存入栈中的数据大小与生存期必须是确定的,因此缺乏灵活性。

    堆是一个运行时数据区,堆中对象通过new、newarray、anewarray和multianewarray等指令创建。堆由GC负责回收,其优势在于动态分配内存大小,生存期也不必事先告诉编译器,他在运行时动态分配内存,GC会回收不再使用的数据。其缺点是存取速度较慢。

    从功能和作用方面来比较,堆主要用来存放对象,栈主要用来执行程序。例如:在C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形参像在工厂的传送带上一样,栈指针会自动指引你到放东西的位置,你所要做的只是把栈指针指向的东西拿下来就行了。在函数退出时,修改栈指针就可以把栈中的内容销毁。在分配数据区时,应该事先知道数据区的大小,虽然分配时在程序运行时进行的,但是分配的大小是确定不变的,这个大小是在编译时就确定好的。
    堆在应用程序运行时请求操作系统给自己分配内存,内存由操作系统管理,因此它在分配和销毁时都要占用时间。但是,编译器不需要知道要从堆里分配多少存储空间,也不需要知道存储的数据要在堆里停留多长时间,所以用堆保存数据将会得到更大的灵活性。由于面向对象的多态特性,其变量的存储空间只有在运行时创建对象之后才能确定,因此堆内存是必要的。

  • Java内存回收策略

    Java语言没有提供直接操作内存的语法,通常显式的内存申请有静态内存分配和动态内存分配。

    • 静态内存分配和回收
      Java中静态内存分配是指在Java被编译时就已经能确定所需要内存空间,当程序被加载时一次性的把内存分配给它。这些内存不会再程序执行时发生变化,直到程序执行结束后,内存被释放。在Java类和方法中的局部变量包括原生数据类型(不包含拆装箱的类型,像Integer,Long等类型都是引用类型是直接继承于Object的,不属于原生数据类型)和对象引用都是静态内存分配。(可以理解为:栈的分配大都使用的是静态内存分配)

    • 动态内存分配和回收
      对象类型(继承于Object)存储于堆中,它们可以被共享,也不一定随着方法结束消失。它的内存空间是动态分配,只有在JVM解析对应引用的Object对象时才知道这个对象中含有哪些信息,都是哪些类型,然后再为其分配相应的存储空间来存储相应的值。这个对象在何时被回收也不确定,只有它不再被使用时才会被回收。

    • 如何检测垃圾
      GC必须能够正确检测出垃圾,同时能够释放垃圾对象所占用的内存。
      我们称不能够被根对象集合达到的对象都是非活动对象,可以被垃圾收集器回收。
      那么,根对象集合中都有什么呢?一般根对象集合会包含以下元素

      • 在方法中局部变量区的对象引用:直接存储在栈帧的局部变量区中的对象
      • Java操作栈中的对象引用:有些对象是直接在操作栈中持有的,所以操作栈肯定也包含根对象集合
      • 在常量池中的对象引用:每个类都会包含一个常量池,其中包含很多对象引用,例如类名字符串,常量池中只会持有这个字符串对象的引用
      • 本地方法中持有的对象引用:一些被传入本地方法中且还没有被释放的对象
      • 类的Class对象:每个类被JVM加载时都会创建一个代表这个类的唯一数据类型的Class对象,这个Class对象存放在堆中,当它不再被使用时,方法区中的类数据和这个Class对象都需要被回收
    • 基于分代的垃圾回收算法
      其设计思路是,按照对象寿命长短来分组,分为年轻代和老年代。新创建的对象被分配在年轻代,当年轻代经历过几次GC之后,仍旧存活则将其放入老年代,这就减少了年轻代GC时扫描对象的数据,从而提高垃圾回收效率。老年代也会进行GC,只是老年代的GC频率比年轻代要小的多。
      整个堆被划分为三个区

      • Young区(年轻代)
        年轻代中又划分了Eden区、From区和To区;对象总是在Eden区中被创建,当Eden区满了后,触发年轻代GC,并将仍旧存活的对象复制到From区,此时Eden区已被清空,只有From区中有对象;当Eden区再次填满时,又发生GC,此时会将Eden区和From区中仍旧存活的对象复制到To区中,在此之后,只有To区中有对象,Eden区和From区被清空;当Eden区又被填满,GC发生,此时将Eden区和To区中仍旧存活的对象复制到From区中,这时Eden区和To区被清空,只有From区有数据,如此循环;虚拟机将始终保证From区或To区有一个是空的,来支持对象复制的进行。
      • Old区(老年代)
        当From或To区满了的同时Eden区也满了,GC会将这些对象直接存放到Old区,当From区或To区中的对象足够老(满足一定GC次数的阈值时),这些对象也会被存放到Old区,直到Old区满了,就会发生Full GC,回收整个内存。
      • Perm区(永久代)
        永久代一般存放类的Class对象,如果一个类被频繁的加载,可能会导致永久代被填满,Perm区也是由Full GC来回收。
    • GC对这些区采用垃圾收集算法也不同,下面是Hotspot提供的三类垃圾收集算法

      • Serial Collector
        Serial Collector是Client模式下的默认GC方式。通过-XX:+UseSerialGC来指定GC使用该算法。它指定所有对象在Eden区中创建,若创建对象超过Eden区大小,或超过-XX:PretenureSizeThreshold配置的大小,就只能在Old区分配。年轻代GC时会检查之前从年轻代晋升到老年代的平均对象大小是否大于老年代的剩余空间(当平均对象大小大于老年代的剩余空间时,则表示每次晋升对象的大小都超过老年代的剩余空间,这说明老年代的空间已经不能满足新对象占用的空间大小,只有触发FullGC来获得更多的空间才行),如果大于则FullGC,如果小于,则看-XX:-HandlePrommotionFailure的配置,如果为true就只触发年轻代GC,否则在触发年轻代GC的同时再触发FullGC。
      • Parallel Collector
        • ParNewGc
          通过-XX:+UserParNewGC参数来指定,它的对象分配和回收策略与Serial Controller类似,区别在于它是多线程并行回收。Parallel Controller有一个UseAdaptiveSizePolicy配置参数,用来动态控制Eden、From区和to区哪些对象经历多少次回收之后可以直接放入Old区。
        • ParallelGC
          server下默认的GC方式,通过-XX:ParalleGCThreads来强制指定,通过-XX:ParallerGCThreads来指定并行回收的线程数(如果CPU小于8核,线程数可以和核数一样,如果大于8核,则值为3+(核心数*5)/8)。
          通过-Xmn来控制Young区的大小,通过SurvivorRatio来设置Eden与Form区的比值大小,-XX:SurvivorRatio=8则表示Eden与From区比值大小为8:1。默认情况下以-XX:InitalSurivivorRatio设置为准,这个值默认为8。
        • ParallelOldGc
          通过-XX:ParallelOldGC参数来强制指定并行回收的线程数可以通过-XX:ParalleGCThreads,其值与ParallelGC的计算方式一致,它与ParallelGC的区别在于,它只清楚heap堆中的部分垃圾对象,并进行空间压缩。而ParallelGC清楚整个Heap堆中的垃圾对象,清除永久代中已经被卸载的类信息,并进行压缩。
      • CMS Collector
        它通过-XX:UseConcMarkSweepGC来指定,默认并发线程4(并行GC线程数+3),也可以通过ParallelCMSThreads指定。它是介于MinorGC和FullGC间的一种GC,当Old区或perm区使用率达到一定比例时,它被触发并回收Old区中的内存空间,比例通过CMSInitiatingOccupancyFraction参数指定,默认为92%。它只回收Old区或Perm区的垃圾对象,回收规则与Serial Collector基本一致,区别是它是多线程的。
  • 内存问题分析

    • 以下参数可以输出GC日志
      • -verbose:gc 辅助输出一些详细的GC信息
      • -XX:+PrintGCDetails,输出GC的详细信息
      • -XX:+PrintApplicationStoppedTime 输出GC造成应用程序暂停的时间
      • -XX:+PrintGCDateStamps GC发生的时间信息
      • -XX:+PrintHeapAtGC 在GC前后输出堆中各个区域的大小
      • -Xloggc:[file] 将GC日志信息输出到单独的文件中
        jstat工具可以用来分析JVMGC运行情况。JVM也是一个程序,它本身也有出bug的可能,但它的退出会在工作目录下产生一个日志文件。

自己使用整理收集,如有侵权 请联系删除!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值