JVM详解

CPU寄存器 - 高速缓存 - 主存(RAM)。处理速度 从 左往右 速度 递减

JVM , JMM ,JDK ,JRE

JDK : Java Development Kit 【Java 软件开发工具包 (SDK)】

JRE : Java Runtime Environment

JMM : Java Memory Model

JVM ::Java Virtual Machine



JDK的安装目录下有一个jre目录,里面有两个文件夹bin和lib,bin里的可以认为就是jvm,lib中则是jvm工作所需要的类库,JVM + lib = JRE

1.JVM的内部结构

中间虚线的是 运行时数据区域 (是一种规范)


每个JVM都有两种机制:

①类装载子系统:装载具有适合名称的类或接口

②执行引擎:负责执行包含在已装载的类或接口中的指令 

内存空间 【组成】 :

JVM内存空间包含:方法区、java堆、java栈、本地方法栈。

方法区  是各个线程共享的区域,存放类信息、常量、静态变量。

java堆 也是线程共享的区域,存放类的实例化和数组的。java堆的空间是最大的。如果java堆空间不足了,程序会抛出OutOfMemoryError异常。

方法区 和 堆的区别 :

堆  存放的是 对象 和 数组 ;

方法区  存的是类信息,静态变量,常量,处理逻辑的指令集。

关系就是 : “方法区 相当于 类”  “堆 相当于 对象”

方法区 逻辑上  在 堆的 Permanent  (永久代 【非堆】)上。物理上 还是在堆上的

JDK1.7 之前 : 字符串常量池 在 方法区 (在堆的永久代)中

JDK1.7:字符串常量池 从 方法区 移到 堆

JDK1.8:字符串常量池 还在方法区 中,但是方法区的实现从 永久代(Perm)变成 元空间(MetaSpace)。

元空间: 本质上与永久代类似,都是JVM规范中方法区的实现。不过 元数据空间并不在虚拟机中,而是使用本地内存。

线程栈 是每个线程私有的区域,用于存储局部变量。它的生命周期线程相同,

本地方法栈 是 为虚拟机的Native方法服务的。

Native方法  :调用非java代码的接口。

使用Native Method的原因 : java应用需要与java外面的环境交互 【比如:与操作系统交互】。

区别:JVM线程栈为 虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

指令计数器,指向当前线程正在执行的字节码指令地址,行号。比如,切换线程后,线程要知道自己原来是在哪里运行的,然后接着运行。

执行引擎  当然就是根据PC寄存器调配的指令顺序,依次执行程序指令。

2.java 堆 的组成结构


Heap = {Old + NEW + Permanent = { Eden , Survivor0, Survivor1 ,Permanent } }

一块是 NEW Generation(新生代), 另一块是Old Generation(老年代). 在New Generation中,有一个叫Eden 的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(Survivor0,Survivor1), 它们用来存放每次垃圾回收后存活下来的对象。在Old Generation中,主要存放应用程序中生命周期长的内存对象【从survivor1 剩下来的内存对象】。

大部分对象在分配时都是在Eden中

较大的对象直接分配到 Old Generation 中

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。 原因: 很可能是创建了大量的对象。


3.垃圾回收算法

①.Mark-Sweep(标记-清除)算法

首先标记出需要回收的对象,标记完成后统一清除对象。

缺点: 效率不高,而且会产生大量的内存碎片。

②.Copying(复制)算法

将可用内存分为两块,每次只用其中的一块,当这块内存用完以后,将还存活的对象复制到另一块上面,然后再把已经使用的内存空间一次清理掉。

优点:效率高,不会产生内存碎片。

缺点:内存缩小为原来的一半

③.Generational Collection(分代收集)算法 【JVM的垃圾收集器采用的算法】

根据不同代的特点采取最适合的收集算法。

新生代都采取Copying算法:因为大部分的对象的创建【且生命周期短】都在Young区。每次垃圾回收都要回收大部分对象。【将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。】

老年代的特点是每次回收都只回收少量对象,一般使用的是标记清除算法。

4.关于内存调优

原因:过多的GC和Full GC是会占用很多的CPU

目的:减少Full GC次数,减少GC频率,尽量降低CG所导致的应用线程暂停时间

手段:主要是针对内存管理方面的调优,包括控制各个代的大小,GC策略

内存控制:

①.旧生代空间不足

调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和 不要 创建过大的对象及数组 避免直接在旧生代创建对象。

②.Pemanet Generation空间不足

增大Perm Gen空间,避免太多静态对象 

③.System.gc()被显示调用

垃圾回收不要手动触发,尽量依靠JVM自身的机制

④.新生代 设置不宜过大 或过小,新生代占整个堆的1/3比较合适。


5.类加载的过程


过程就是类加载器将 所需的 (.class文件)字节码文件中要执行的代码逻辑以指令的形式 加载到 方法区,调用java方法就通过,java堆,java栈。调用native方法,就通过本地方法栈。最后执行引擎再通过PC寄存器上指令的执行顺序进行执行。


1.1 加载

加载主要是将.class文件中的二进制字节流读入到JVM中。

在加载阶段,JVM需要完成3件事:

1)通过类的全限定名获取该类的二进制字节流; 

2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构; 

3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 

1.2 连接

①. 验证

验证是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。

②.准备

为静态变量在方法区分配内存,并设置默认初始值

③.解析

虚拟机将常量池内的符号引用替换为直接引用的过程

④.初始化

类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值

小知识:类加载器:类加载器实现的功能是为加载阶段获取二进制字节流的时候。


以上为 :双亲委派模型。

概念:如果一个类接受到类加载请求,他自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器。 只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

类加载器的作用:将.class文件加载到jvm的内存空间中。

双亲委派模式的代码实现:

1)首先检查类是否被加载,没有则调用父类加载器的loadClass()方法; 

2)若父类加载器为空,则默认使用启动类加载器作为父加载器; 

3)若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

面试题:自己写的java.lang.String类能否被加载?

根据上边的原则 1), 我们自己写的String应该是被Bootstrap ClassLoader(顶层类加载器)加载了,所以App ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。


6.JMM :JAVA 内存模型 ,JVM : Java虚拟机模型。

JMM:包括 本地内存 (线程),主内存 。

本地内存 存放 着 主内存的共享变量的副本。线程在 本地内存 中运算后,将结果刷到主内存。如果线程A 与 线程B 都同时操作,这就是多线程问题的由来了。

Java 内存模型 【JMM】的抽象图:


JVM :包括方法区,堆,虚拟机栈,本地方法栈,PC寄存器。

线程共享 : 方法区,堆

线程私有:虚拟机栈(执行java方法),本地方法栈(执行native方法),PC寄存器(指令执行顺序)

7.AtomicInteger 提供原子操作的Integer类,不会有线程安全问题。十分适合高并发情况下。

保证 原子性 的秘诀 :基于CAS【比较交换】

CAS的思想很简单:主内存值 M,期望值 V,待更新值 U,Only M =V ,update V 去主内存,M≠V ,则继续对V+ 1,U+ 1,再去取主内存值M,直到V=M ,才将更新值进行更新

假设线程1和线程2通过getIntVolatile拿到value的值都为1,线程1被挂起,线程2继续执行

线程2在compareAndSwapInt操作中由于预期值和内存值都为1,因此成功将内存值更新为2

线程1继续执行,在compareAndSwapInt操作中,预期值是1,而当前的内存值为2,CAS操作失败,什么都不做,返回false

线程1重新通过getIntVolatile拿到最新的内存value为2,再进行一次compareAndSwapInt操作,这次操作成功,因为取了最新的值做为预期值,预期值是2,内存值也是2,加1更新后内存值更新为3

8.线程池:为了避免创建和销毁进程,对内存的开销,用线程池,给定数量的线程,当有请求来到,直接从线程池请求,分配线程执行请求的任务。


©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 所有知识点已整理成app app下载地址 J2EE 部分: 1.Switch能否用string做参数? 在 Jav...
    侯蛋蛋_阅读 7,303评论 1 4
  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 4,908评论 0 2
  • 这篇文章解释了Java 虚拟机(JVM)的内部架构。下图显示了遵守Java SE 7 规范的典型的 JVM 核心内...
    饮墨飨书阅读 4,162评论 0 1
  • 工作之余,想总结一下JVM相关知识。 Java运行时数据区: Java虚拟机在执行Java程序的过程中会将其管理的...
    Huang远阅读 3,788评论 0 2
  • 荀子在《劝学》中,主要通过比喻论证来阐述他对“学习不可以停止”的见解,体现在文中的三个分论中。 分论点一:学习的重...
    xj明明如月阅读 27,424评论 0 2