JVM学习笔记2.0

1.运行时数据区域

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

  • 程序计数器

        程序计数器(Program Counter Register)是一块较小的内存空向,它可以看作是当前线程所执行的字节码的行号指示器。在虚似机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作肘就是通过改变这个计数器的値来选取下一条需要抉行的字节码指令分支、循环、跳装、异常处理、线程恢夏等基础功能都需要依赖这个计数器来完成。
        如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。 此内存区域是唯- 一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

  • Java虚拟机栈

        Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中人栈到出栈的过程。
        在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

  • 本地方法栈

        本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别是虚拟机栈为虚拟机执行Java方法( 也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
        本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

  • Java堆

        Java堆是垃圾收集器管理的主要区域。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。如果在堆中没有完成实例分配,并且堆也再无法扩展时,将会抛出OutOfMemoryError异常。

  • 方法区

        与堆一样,也是各个线程共享的一块内存区域。特别地,在HotSpot虚拟机上,很多人称方法区为永久代。,当无法再申请到内存时,会抛出OutOfMemoryError异常。

  • 运行时常量池

        是方法区的一部分,同样受到方法区内存的限制,当无法再申请到内存时,会抛出OutOfMemoryError异常。

  • 直接内存

        直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
        NIO可以直接分配堆外内存

2.hotspot中对象的内存布局

Hotspot虚拟机中,对象在内存中的存储布局分为三部分:

    Header(对象头)
    
        自身运行时数据(32位~64位MarkWord):哈希值、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳。
        类型指针
        
    InstanceData:数据实例,即对象的有效信息,相同宽度(如long和double)的字段被分配在一起,父类属性在子类属性之前。    

    Padding:占位符填充内存

3.对象的访问定位

  • 句柄访问

    最大好处是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

  • 直接指针访问

    最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。Hotspot使用直接指针访问。

4.内存溢出OutOfMemoryError

  • Java堆溢出

    Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
    Java堆内存的0OM异常是实际应用中常见的内存溢出异常情况。当出现Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进.步提示“Java heap space"。
    如果是内存泄露,可进一步通过查看泄露对象到GC Roots的引用链,就能找到泄露对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GCRoots引用链的信息,就可以比较准确地定位出泄露代码的位置。
    如果是不存内存在泄露,应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

  • 栈溢出

    hotspot虚拟机并不区分虚拟机栈和本地方法栈。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMermoryEror异常。
    在单线程下,无论是由于栈帧太大还是虛拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。多线程情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

  • 常量区和方法区

    提示:java.lang.OutOfMemoryError:PermGen space

5.垃圾回收(GC)

  • 标记清除算法

    主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高:另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  • 复制算法(多用于新生代)

    将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的900%(80%+10%),只有10%的内存会被“浪费”。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。
    复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

  • 标记整理算法(多用于老年代)

根据老年代的特点,提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向-端移动,然后直接清理掉端边界以外的内存。

  • 分代收集算法

    Java 虚拟机将堆分为新生代和老年代,并且对不同代采用不同的垃圾回收算法。其中,新生代分为 Eden 区和两个大小一致的 Survivor 区,并且其中一个 Survivor 区是空的。
    在只针对新生代的 Minor GC 中,Eden 区和非空 Survivor 区的存活对象会被复制到空的 Survivor 区中,当 Survivor 区中的存活对象复制次数超过一定数值时,它将被晋升至老年代。
    因为 Minor GC 只针对新生代进行垃圾回收,所以在枚举 GC Roots 的时候,它需要考虑从老年代到新生代的引用。为了避免扫描整个老年代,Java 虚拟机引入了名为卡表的技术,大致地标出可能存在老年代到新生代引用的内存区域。

6.对象分配

对象分配原则:   
1.对象优先在eden分配
2.大对象直接进入老年代
3.长期存活的对象将进入老年代
4.动态对象年龄判定

7.类加载

    1.七个阶段:
    加载,验证,准备,解析,初始化,使用,卸载。验证,准备,解析三个部分统称为连接。

    2.对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

    3.类加载器类型

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application ClassLoader)

    4.双亲委派模型

    双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器丢完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到项层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
    使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

    5.破坏双亲委派模型

    双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但是如果基础类又要调用回用户的代码时,启动类加载器不“认识”这些代码,为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLcader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
    有了线程上下文类加载器,就可以使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的-般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、 JCE、 JAXB和JBI等。

8.Java语法糖

  • 泛型擦除
  • 自动拆装箱
  • 遍历循环
  • 条件编译

猜你喜欢

转载自blog.csdn.net/TP89757/article/details/105191748