秋招-Java-JVM 与 JMM篇

秋招-Java-JVM 与 JMM篇

JVM

JVM是什么

基本信息

JVM是JavaVirtualMachine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

JVM体系结构

  • 方法区 方法区存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据;
  • 堆 仅有一个堆,Java堆用于存放new出来的对象的内容。
  • 栈 存放的东西:八大基本类型 + new出来的对象引用地址 + 实例方法的引用地址。
  • 程序计数器 程序计数器记录【当前线程所执行的Java字节码的地址】。

类在JVM中的加载过程

类加载过程实质是读取.class文件生成对应的java.long.Class对象,并将类的静态数据结构初始化的过程。

以下2,3,4统称【连接】 1.加载 通过一个类的全限定名获取定义此类的二进制字节流,并将该流的数据存储到方法区中。 2.验证 这一阶段主要是为了确保Class文件的字节流中包含的信息符合虚拟机的要求,并且不会危害虚拟机自身的安全。 3.准备 是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区分配 4.解析 是将虚拟机常量池内的符号引用替换为直接引用的过程.(用内存引用替代io引用) 5.初始化 类初始化的主要工作时为了静态变量赋程序设定的初值

类加载器

作用是加载.class文件,将新建的对象放入堆里面,引用(地址)放到栈,其中引用指向堆里面对应的对象。

分类: 1)启动类(根)加载器 Bootstrap ClassLoader:负责加载jre\lib目录下的rt.jar包 2)扩展类加载器 Extension ClassLoader:负责加载jre\lib\ext目录下的所有jar包 3)应用程序(系统类)加载器 Application ClassLoader:负责加载用户类路径上所指定的类库,如果应用程序中没有自定义加载器,那么次加载器就为默认加载器。

常见机制

双亲委派机制

  • 概述 类的加载器会优先由更高级的类加载器加载。
  • 工作过程
  1. 类加载器收到类加载的请求;
  2. 把这个请求委托给父加载器去完成,一直向上委托,直到启动类(根)加载器;
  3. 启动类加载器检查能不能加载(使用findClass()方法),能加载就结束;否则抛出异常,通知子加载器进行加载;
  4. 重复步骤三.
  • 机制测试 测试代码:
package java.lang;

public class String {

    public static String toStrings() {
        return "Hello";
    }

    public static void main(String[] args) {
        System.out.println(String.toStrings());
    }
}

报错如下:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
  • 原因分析 由于双亲委派机制,这个假String类被根加载器捕获,由根加载器加载尝试加载了真正的String,但真String没有main方法,所以报错。 这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader), 所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

沙箱安全机制

  • 概述 沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。 CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
  • 组成 1.字节码校验器 确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

2.类装载器 类装载器在3个方面对Java沙箱起作用:

它防止恶意代码去干涉善意的代码; 它守护了被信任的类库边界; 它将代码归入保护域,确定了代码可以进行哪些操作。 类装载器采用的机制是双亲委派模式。

3.存取控制器 存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

4.安全管理器 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。

5.安全软件包 java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者、消息摘要、数字签名、加密、鉴别等。

JVM组件

Native、本地方法栈

  • 概述 native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!会进入本地方法栈,然后通过本地接口 (JNI)( Java Native Interface ),调用本地方法库。 本地方法栈:在 Native Method Stack 中登记 native方法,在执行引擎执行的时候通过本地接口 (JNI),加载本地方法库(Native Libraies)。
  • 作用 开拓Java的使用,融合不同的编程语言为Java所用,不过目前native方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java程序驱动打印机 或者 Java系统管理生产设备,在企业级应用中已经比较少见。而且现在的异构领域间通信很发达,比如可以使用 Socket通信,也可以使用 Web Service 等等。

程序计数器

  • 每个线程都有一个程序计数器,是线程私有的
  • 就是一个指针, 指向方法区中的方法字节码(用来存储指向一条指令的地址——将要执行的指令代码,执行引擎读取下一条指令
  • 是一个非常小的内存空间,几乎可以忽略不计

方法区

  • 方法区存放被所有线程共享的所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
  • 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量和数组的内容存在堆内存中,和方法区无关

栈有可能放的东西:8大基本类型 + 对象的引用 + 实例的方法

栈运行的原理:每执行一个方法,都会产生一个栈帧(可以理解为一个对象,包括:方法索引,输入输出参数,本地变量,Class File,父帧,子帧)

栈如果满了,就会 StackOverFlowError

(1) 堆内存分为年轻代(Young Generation)、老年代(Old Generation)

年轻代又分为Eden和Survivor区。

  • Survivor区由FromSpace和ToSpace组成。
  • Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。Eden满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,
  • 幸存者区满之后意味着新生区也满了,则触发重GC,经过重GC之后存活下来的就到了老年代。

(2) 堆内存用途: 存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

(3) 堆与分代 新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。 老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。 Minor GC : 清理年轻代 Major GC : 清理老年代 Full GC : 清理整个堆空间,包括年轻代和永久代 所有GC都会停止应用所有线程。

(4) GC处理流程:

  • 伊甸园满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,
  • 幸存者区满,意味着新生区也满了,
  • 则触发重GC,对整个新生区进行清理
  • 经过重GC之后存活下来的就到了养老区。
  • 连养老区也满了,说明整个内存都满了

元空间

  • 在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
  • 元空间有注意有两个参数: MetaspaceSize :初始化元空间大小,控制发生GC阈值 MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
  • 为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。有了元空间就不再会出现永久代OOM问题了!

堆内存调优

格式

格式:-XX:[+-]<name> 表示启用或者禁用name属性。
例子:-XX:+UseG1GC(表示启用G1垃圾收集器)

操作

  • JVM内存分析 默认情况下: 分配的总内存,是电脑内存的 1/4,而初始化 JVM 的内存是 1/64。
  • JVM内存排错 遇到OOM:堆空间错误

1.尝试扩大堆内存(设置 VM Options),查看结果: -Xms1024m -Xmx1024m -XX:+PrintGCDetails

如果解决,说明是默认分配的内存不够 2.分析内存,看一下哪里出现了问题(专业工具)

内存快照分析工具: Jprofile、 MAT (eclipse集成使用),分析dump出来的文件 工具作用: 分析 Dump 内存文件,快速定位内存泄露 探知堆中的数据,获得大的对象 …

垃圾回收算法GC

  1. 引用计数算法 在JVM中几乎不用,每个对象在创建的时候,就给这个对象绑定一个计数器(有消耗)。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。 优点:
  • 简单
  • 计算代价分散
  • “幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态) 缺点:
  • 不全面(容易漏掉循环引用的对象)
  • 并发支持较弱
  • 占用额外内存空间(计数器消耗)
  1. 复制算法 每次轻 GC 之后,Edan区是空, To 区是空。将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。 这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就行,实现简单,运行高效。
  2. 标记-清除算法 为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。 优点:
  • 实现简单,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。
  • 此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置。 缺点:
  • 需要进行两次动作,标记获得的对象和清除死亡的对象,所以效率低。
  • 死亡的对象被GC后,内存不连续,会有内存碎片,GC的次数越多碎片越严重。
  1. 标记-压缩/整理算法 标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段不是进行直接清理,而是令所有存活的对象向一端移动,然后直接清理掉这端边界以外的内存。

优点:不会像标记-清除算法那样产生大量的碎片空间。

缺点: 如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。

  1. 总结 指标比较:

内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记压缩算法 内存整齐度:标记压缩算法 = 复制算法 > 标记清除算法 内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

GC:分代收集算法

年轻代特点存活率低,所以适合于复制算法; 老年代存活率高,适合于标记清除+标记压缩混合实现

JMM

Java内存模型(Java Memory Model)

JMM是用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。

内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型,Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。

它不是对物理内存的规范,而是在虚拟机基础上进行的规范从而实现平台一致性,以达到Java程序能够“一次编写,到处运行”。

Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。

三大特性

原子性(Atomicity)

一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。

可见性:

一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。 通常使用volatile关键字来处理,volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。

使用Lock接口的最常用的实现ReentrantLock(重入锁)来实现可见性:当我们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值。

有序性:

对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。 Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现 在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。

voltile

作用

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile和static

1.static保证唯一性,就是在主内存中是唯一的变量。

2.volatile是保证可见性,就是指在工作线程和主内存的数据的一致性,改变了工作线程中volatile修饰的变量,那么主内存也要发生更新。

volatile和static一起使用不矛盾。因为static修饰只能保证在主内存的唯一性,如果涉及到其他工作线程,改变参数可能就会导致static修饰的变量的内容无法同步,所以static和volatile可以一起使用,因为他们管的地方是不一样的,互不影响。

猜你喜欢

转载自blog.csdn.net/qq_50665031/article/details/126098790