java内存模型

Java内存模型(JMM):
  • 简称JMM,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
  • 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个稀有的本地内存(local memory),本地内存中存储了该线程以 读 / 写共享变量的副本。
  • 内存可见性:
    在这里插入图片描述
  • (在双重检验锁(DLC)中,就使用到了 volatile,防止new对象的时候出现指令重排序的问题,)(volatie的第二个作用:强制主内存读写同步)volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。
Java虚拟机(JVM):
  • 是什么:是一个可以执行Java字节码的虚拟机进程。Java源文件能被编译成能被Java虚拟机执行的字节码文件。

  • 作用:解释运行字节码程序消除平台相关性。(Java被设计成允许应用程序可以运行在任意平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为他知道底层硬件平台的指令长度和其他特性。)
    在这里插入图片描述

  • 线程私有:

    • (虚拟机栈)栈(Stack)【先进后出】

      1. 线程私有,生命周期与线程相同(创建,就绪,运行,阻塞,死亡)

      2. 栈是java方法执行的内存模型:

        每个方法执行时都会在栈中创建一个栈帧(stack fream),用于存放局部变量类、操作栈、动态链接、方法出口等信息

    • 本地方法栈(Nactive Method Stack)

      1. 与java虚拟机功能类似。只不过这个是服务于JVM使用的nactive方法
    • 程序计数器(Program Counter Register)

      1. 每个线程执行时都有自己的程序计时器,互不影响。
      2. 程序计数器指向该计数器的拥有者(线程)下一步执行的位置。
  • 线程共享:

    • 堆(Heap)【先进先出】
      1. 内存中最大的一块区域,堆是各线程共享内存的区域。
      2. 堆中存放被创建的实例对象、数组。
      3. 堆是GC管理的主要区域
    • 方法区(Method Area)
      1. 存放类的信息、静态变量、常量(常量池包含于方法区)、即时编译器编译后的代码数据
  • 堆内存被分代管理:为什么要分代管理?

    • 主要是方便垃圾回收(1.大部分对象使用时间很短,2.有些对象不会立即无用,但也不会持续很长时间)

在这里插入图片描述

垃圾回收(GC)算法
  • Garbage Collection(垃圾收集),其对象是堆空间和永久区
  1. 标记-清除:

    标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发full gc。

  2. 复制算法

    将现有的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

    现在的商业虚拟机都采用这种收集算法来回收新生代,IBM研究表明新生代中的对象98%是朝夕生死的,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一个Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1(可以通过-SurvivorRattio来配置),也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

  3. 标记-压缩(标记-整理)

    复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

    标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

  4. 增量算法

    增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

Minor GC / Young GC
  • 年轻代需要回收时就会触发 Minor GC(也称作Young GC)。当Eden区满时,触发Minor GC
  • 年轻代是由 Eden Space 和两块相同大小的 Survivor Space(又称 From Space 和 To Space)构成,Eden 和 Servior 区的内存比为8:1,可通过 -XMN参数调整新生代大小。
  • 年轻代的Eden区内存是连续的,所以其分配会非常快,同样Eden区的回收也非常快(因为大部分情况下Eden区对象存活时间非常短,而Eden区采用的复制回收算法,此算法在存活对象比例很少的情况下很高效)。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能在扩展,将会抛出 OutOfMemoryError:Java Heap Space 异常。
Full GC
  • 老年代的垃圾回收称之为Full GC。所占内存大小为 -Xmx 对应的值减去 -Xmn所对应的值
  • 触发条件:
    1. 调用System.gc() 时,系统建议执行Full GC,但是不一定肯定去执行
    2. 老年代空间不足
    3. 方法区空间不足
    4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    5. 由 Eden区、From Space区向 To Space区复制时,对象大小大于 To Space可用内存(大对象),且老年代的可用内存大于该对象大小,则吧该对象转存到老年代,
为什么称 Java 为 “平台无关的编程语言”?
  • JVM将java字节码解释为具体平台的具体指令。一般的高级语言如果需要在不同的平台上运行,至少需要编译成不同的目标代码。但是引入JVM之后,Java语言在不同平台上运行时不需要重新编译。java语言在使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改的运行。Java虚拟机在执行字节码时,吧字节码解释成具体平台上的机器指令执行。
类的加载过程:

在这里插入图片描述

  1. 加载(Loading)【文件到内存】

    • 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据入口。注意这里不一定非得要从一个class文件获取,既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其他文件生成(比如将JSP文件转换成对应的Class类)。
  2. 验证(Veridication)

    • 主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  3. 准备(Preparation)

    • 正式为类变量分配内存并设置类变量的初始设置阶段,即在方法区中分配这些变量所使用的内存空间。
  4. 解析(Resolution)

    • 虚拟机将常量池中的符号引用替换为直接引用的过程。
    • 符号引用:就是class文件中的 CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info 等类型的常量。
    • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
    • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
  5. 初始化(Initialization)

    • 类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其他操作都是由JVM主导。到了初始化阶段,才开始真正执行类中定义的Java程序代码

    • 初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法

      • 以下几种情况不会执行类初始化
        1. 通过子类引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化。
        2. 定义对象数组,不会触发该类的初始化。
        3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接定义常量的类,不会触发定义常量所在的类。
        4. 通过类名获取Class对象,不会触发类的初始化
        5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否对类进行初始化。
        6. 通过ClassLoader默认的loadClass方法,不会触发初始化动作
    • 类加载器:虚拟机设计团队吧加载动作放到JVM外部实现,以便让应用程序决定如果获取所需的类,JVM提供了3种类加载器。

      1. 启动类加载器(Bootstarp ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或者通过-Xbootclasspath参数指定路径中,且被虚拟机认可(如文件名识别,如rt.jar)的类。
      2. 扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME\LIB\ext目录中的,或者通过 java.ext.dirs系统变量指定路径中的类库。
      3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
    • JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

在这里插入图片描述

  • 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,CIA会尝试执行加载任务。采用双亲委派的一个好处就是:比如加载位于rt.jar包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不用填的类加载器最终得到的都是同样一个Object对象
JVM加载class文件的原理
  • JVM中的类的装载是由ClassLoader和他的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。他负责在运行时查找和装载类文件的类。

  • Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载本身也是一个类,而他的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们使用特殊的用法,比如:反射,就需要显式的加载所需的类。

  • 类加载方式,有两种

    1. 隐式装载:程序在运行过程中当碰到通过 new 等方式生成对象时隐式调用类装载器加载对应的类到JVM中

    2. 显式装载:通过Class.forName() 等方法,显式加载需要的类。

      隐式加载与显式加载:两者本质是一样的。

  • Java类的加载时动态的,他并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(比如:基类)完全加载到JVM中,至于其他类,则在需要的时候才加载。这当然是为了节省内存开销。

内存泄漏
  • 什么是内存泄漏:不再被使用的对象内存不能被回收。如果长生命周期的对象持有短生命周期的引用,就很有可能出现内存泄漏。Java中存在内存泄漏,并且有可能会变得相当严重。

  • Java garbage collector (GC:垃圾收集)自动释放那些内存里面程序不再需要的对象,以此避免大多数的其他程序上下文的内存泄漏。

异常

在这里插入图片描述

  • 运行时异常
    • 运行时异常都是由 RuntimeException 类及其子类异常,如NPE(NullPointerException),这些异常是不受检查的,在程序中可以选择捕获,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑上尽可能避免这类异常的发生。
  • 受检性异常( 非运行时异常 )
    • 受检性异常是RuntimeExceptioin以外的异常,类型上都是属于Exception类及其子类。如IOException等以及用户自定义的Exception异常。对于这种异常,,Java编译器强制要求我们必需对出现的这些异常进行catch并处理,否则程序就不能编译通过。所以,对于这些异常,我们必需写catch去处理可能的异常。
序列化
  • 一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可以将流化后对象传输与网络之间。序列化是为了解决在对对象进行读写操作时所引发的问题。
  • 序列化的实现:将被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象可被序列化,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutStream(对象流)对象,然后使用ObjectOutStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话就用输入流
强软弱虚
  1. 强引用:一个对象赋给一个引用就是强引用,比如new一个对象,一个对象被赋值一个对象。

  2. 软引用:用SoftReference类实现,一般不会轻易回收,只有内存不够才会回收。

  3. 弱引用:用WeekReference类实现,一旦垃圾回收已启动,就会回收。

  4. 虚引用:不能单独存在,必须和引用队列联合使用。主要作用是跟踪对象被回收的状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值