JVM入门

一、JVM整体架构

  • Jvm(虚拟机):以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统,是物理机的软件实现。常用VMWarea Virtual Box Java Virtual machine
  • 常见Java虚拟机阵营
    • HotSpot VM (Sun)
    • JRockit(BEA System)
    • IBM J9(IBM)

Jvm主要由3个子系统组成

  • 类加载器
  • 运行时数据区(内存结构)
  • 执行引擎

Java运行时编译源码(.java)成字节码,由jre运行。jre由java虚拟机(jvm)实现。Jvm分析字节码,然后解析并执行。

二、类加载器

类加载过程

类加载:将class文件加载到虚拟机的内存

  • 加载:在硬盘上查找并通过IO读入字节码文件
  • 连接:执行验证、准备、解析(可选)步骤
    • 验证:校验字节码文件的正确性
    • 准备:给类的静态变量分配内存,并赋予默认值
    • 解析:类装载器装入类所引用的其他所有类
  • 初始化:对类的静态变量初始化为指定值,执行静态代码块

1、类加载器种类#

  • 启动类加载器 (BootstrapClassLoader):负责加载JRE的核心类库,如jre目录下的rt.jar、charsets.jar等
  • 扩展类加载器 (ExtClassLoader):负责加载JRE扩展目录中的JAR包
  • 系统类加载器 (AppClassLoader):负责加载ClassPath路径下的类包
  • 用户自定义加载器:负责加载用户自定义路径下的类包

2、类加载机制#

  • 全盘负责委托机制:当一个ClassLoader加载一个类时,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
  • 双亲委派机制:指先委托父类加载器寻找目标类,在找不到的情况下在自己路径中查找并载入目标类

双亲委派机制优势

  • 沙箱安全机制:例-自己写的java.lang.String.class类不会被加载,防止核心API被随意篡改
  • 避免类的重复加载:当父类已经加载了该类时,那么子类就没有必要在加载一次了

3、类加载过程#

JVM对class文件是按需加载(运行期间动态加载),非一次性加载。

三、运行时数据区

1、方法区#

线程共享

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义(所有定义的方法的信息都保存在该区域)。

静态变量、常量、类信息(构造函数,接口定义)、运行时常量池都保存在方法区

2、栈#

线程私有不存在垃圾回收问题

存放的类型:8种数据类型、对象的引用、实例的方法

java线程执行方法的内存模型,一个线程对应一个栈,每个方法执行的同时都会创建一个栈帧(存储局部变量表,操作数栈,动态链接,方法出口等信息),只要线程一结束该栈就释放,声明周期和线程一致

栈运行原理:栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。

3、本地方法栈#

Native

/**
 * native:凡是带用native关键字的,代表Java作用范围达不到了,需要回去调用底层C语言的库
 * 会进入本地方法栈 调用本地方法接口(JNI)
 * JNI作用:扩展JAVA程序,融合不同的编程语言为Java所用 最终是为了融合C、C++
 * Java诞生的时候 C、C++语言很流行,Java语言想要有立足之地的话就必须能够调用C、C++的程序
 * 它在内存区域中专门开辟了一块标记区域:Native Method Stack 登记native方法
 * 在最终执行的时候,通过JNI加载本地方法库中的方法
 *
 * 现在在开发中很少用到native了(适用场景:Java驱动打印机等)
 */
public native void start0();
  • Java虚拟机栈用于关联java方法的调用,而本地方法栈用于管理本地方法的调用
  • 线程私有
  • 本地方法就是java中native修饰的方法,调用C实现
  • 本地方法栈中等级native方法,在执行引擎执行时加载本地方法库
  • 并不是所有虚拟机都支持native因为在jvm规范中没有明确要求
  • 在HotspotJVM中,直接将本地方法栈和虚拟机栈合二为一了。

4、程序计数器(PC寄存器)#

每个线程都有一个线程计数器,线程私有,

一个线程对应一个 JVM Stack。JVM Stack 中包含一组 Stack Frame。当 JVM 调用一个 Java 方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入 JVM 栈中。

在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

4.1、作用#

  • PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令
  • 内存空间很小,几乎可以忽略不记。运行速度最快的存储区域

4.2、问题#

  • 使用PC寄存器存储字节码指令地址有什么用?(为什么使用PC寄存器记录当前线程的执行地址?)

    (1)多线程宏观上是并行,但实际上是并发交替执行的
    (2)因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
    (3)JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
    
  • PC寄存器为什么会设定为线程私有?

    多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,为每一个线程都分配一个PC寄存器,这样各个线程之间就能够进行独立计算,从而不会出现相互干扰的情况
    
  • CPU时间片

    CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片
    在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行
    微观上:由于只有一个CPU,一次只能处理程序要求的一部分,为了公平切分时间片,使每个程序轮流执行
    

众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

5、堆#

一个JVM只有一个堆内存,堆内存的大小是可以调节的。

堆内存划分:

  • 新生区

    • 伊甸园区(Eden)
    • 幸存区(0/1)
  • 养老区

  • 永久区(元空间)

    用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息~
    这个区域不存在垃圾回收关闭VM虚拟就会释放这个区域的内存~

GC:Garbage recycling

轻GC:主要在新生区

重GC(Full GC):主要在养老区,说明内存快爆了

内存测试#

VM options配置 -Xms8m -Xmx8m -XX:+PrintGCDetails

public class HeapTest {
    public static void main(String[] args) {
        // 虚拟机试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        // 虚拟机初始化总内存
        long total = Runtime.getRuntime().totalMemory();
        System.out.println(max + "字节   " + max / (double) 1024 / 1024 + "M");
        System.out.println(total + "字节   " + total / (double) 1024 / 1024 + "M");
    }
}

小结:永久区逻辑上存在,物理上不存在

四、堆内存调优

IDea中,可以通过调整这个参数(Edit Configuration—>VM options)控制Java虚拟机初始内存和分配的总内存的大小。

默认情况下:

分配的总内存是电脑内存的 1/4,而初始化的内存: 1/64

OOM(OutOfMemoryError):内存溢出

解决:

1、尝试扩大内存查看结果

2、分析内存,(可以使用Jprofile工具)

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

Jprofile作用:

  • 分析Dump内存文件,快速定位内存泄露

    • 生成dump文件,参数配置

      -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
      
  • 获得堆中的数据

  • 获得大的对象

    ..........

4.1、常有调优参数#

开发过程中,通常会将-Xms与-Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源
-Xms:表示java虚拟机堆区内存初始内存分配的大小,通常为操作系统可用内存的1/64大小即可,但仍需按照实际情况进行分配
-Xmx:表示java虚拟机堆区内存可被分配的最大上限,通常为操作系统可用内存的1/4大小。

-XX:newSize:表示新生代初始内存的大小,应该小于-Xms的值
-XX:MaxnewSize:表示新生代可被分配的内存的最大上限;当然这个值应该小于-Xmx的值
-Xmn:至于这个参数则是对 -XX:newSize、-XX:MaxnewSize两个参数的同时配置,也就是说如果通过-Xmn来配置新生代的内存大小,那么-XX:newSize = -XX:MaxnewSize = -Xmn,虽然会很方便,但需要注意的是这个参数是在JDK1.4版本以后才使用的

java虚拟机对非堆区内存配置的两个参数:
-XX:PermSize:表示非堆区初始内存分配大小(方法区)
-XX:MaxPermSize:表示对非堆区分配的内存的最大上限(方法区)


-XX:SurvivorRatio JVM参数中有一个比较重要的参数SurvivorRatio,它定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10



-XX:+AlwaysPreTouch 服务启动的时候真实的分配物理内存给jvm
首先要简单的了解一下,虽然通过JVM的参数-Xmx和-Xms可以设置JVM的堆大小,但是此时操作系统分配的只是虚拟内存,只有JVM真正要使用该内存时,才会被分配物理内存
逻辑会影响哪些方面
1、对象首先会先分配在年轻代,因为之前分配的只是虚拟内存,所以每次新建对象都需要操作系统来先分配物理内存,分配对象速度自然就降低了,只有等第一次新生代GC后,该被分配的内存空间都已经分配了,之后分配对象的速度才会加快。
2、那么老年代也是同理,老年代的空间何时真正使用,自然是对象需要晋升到老年代时,所以新生代GC的时候,对象要从新生代晋升到老年代,操作系统也需要为老年代先分配物理内存,这样就间接影响了新生代GC的效率。

使用【-XX:+AlwaysPreTouch】参数能够达到的效果就是,在服务启动的时候真实的分配物理内存给JVM,而不再是虚拟内存,效果是可以加快代码运行效率,缺点也是有的,毕竟把分配物理内存的事提前放到JVM进程启动时做了,自然就会影响JVM进程的启动时间,导致启动时间降低几个数量级。

-XX:+UseG1GC JVM 指定使用垃圾回收策略,使用G1垃圾回收器可以
 
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

-XX:InitiatingHeapOccupancyPercent=45 – 整个堆栈使用达到百分之多少的时候,启动GC周期. 基于整个堆,不仅仅是其中的某个代的占用情况,G1根据这个值来判断是否要触发GC周期, 0表示一直都在GC,默认值是45(即45%慢了,或者说占用了)

-XX:G1HeapRegionSize=n	G1 GC的堆内存会分割成均匀大小的区域,这个值设置每个划分区域的大小,这个值的默认值是根据堆的大小决定的。最小值是1Mb,最大值是32Mb


-XX:ConcGCThreads 并发收集的时候使用多少个线程,默认值和平台有关。(并发处理的时候使用多少个线程)

-XX:G1HeapWastePercent=10
设置您愿意浪费的堆百分比。如果可回收百分比小于堆废物百分比,Java HotSpot VM 不会启动混合垃圾回收周期。默认值是 10%。Java HotSpot VM build 23 中没有此设置。

-XX:+UseTLAB
表示,是否使用TLAB
-XX:+ScavengeBeforeFullGC
在执行 Full GC 前执行一次 Minor GC可以较少老年代中对象“意外”存活的现象。
这些老年代对象被新生代中的对象所引用, 但这些新生代对象其实已经可以被回收了,所以这些老年代对象其实也应被回收
-XX:+DisableExplicitGC  默认启用  禁止在运行期显式地调用System.gc()
如果jvm参数中设置了-XX:+DisableExplicitGC,那么代码中手动调用System.gc()就不会生效。而有些框架中因为是使用的堆外内存,必须手动调用System.gc()来释放。如果禁用掉就会导致堆外内存使用一直增长,造成内存泄露。

 -XX:+PrintGCDetails 打印GC日志信息
 -XX:+PrintGCDateStamps 打印GC时间 
 -Xloggc:logs/gc.log 将GC日志输出到指定的文件中
 -XX:-UseGCOverheadLimit  JDK6新增错误类型。当GC为释放很小空间占用大量时间时抛出。一般是因为堆太小。导致异常的原因:没有足够的内存。

五、GC(Garbage recycling)

JVM进行垃圾回收的区域:新生代(Eden、Survivor from、Survivor to)、老年代。大部分GC都在新生代中发生。

新生代发生的GC叫Major GC,老年代发生的GC叫Full GC,Full GC至少伴随着一次Major GC

1、GC算法#

  • 引用计数法
  • 复制算法
  • 标记清除算法
  • 标记整理算法

2、GC算法小结#

内存效率(时间复杂度):复制算法>标记清除算法>标记整理算法
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
   
GC使用分代收集算法

年轻代:存活率低,所以采用复制算法
老年代:区域大,存活率高 使用标记清除算法+标记整理算法混合实现
posted @   胡同咖啡  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示
主题色彩