JVM内存管理剖析

5 篇文章 0 订阅

一.JVM与操作系统的关系

1.Java Virtual Machine

JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。

2.翻译

Java 程序不一样,使用 javac 编译成 .class 文件之后,还需要使用 Java 命令去主动执行它,操作系统并不认识这些 .class 文件。所以JVM就是一个翻译。

在这里插入图片描述
从图中可以看到,有了 JVM 这个抽象层之后,Java 就可以实现跨平台了。JVM 只需要保证能够正确执行 .class 文件,就可以运行在诸如 Linux、Windows、MacOS 等平台上了。

3.从跨平台到跨语言

跨平台: 我们写一个类Person这个类,在不同的操作系统上(Linux、Windows、MacOS 等平台)执行,效果是一样,这个就是JVM的跨平台性。
为了实现跨平台型,不同操作系统有不同的JDK的版本

跨语言: JVM只识别字节码,所以JVM其实跟语言是解耦的,也就是没有直接关联,并不是它翻译Java文件,而是识别class文件,这个一般称之为字节码。还有像Groovy 、Kotlin、Jruby等等语言,它们其实也是编译成字节码,所以也可以在JVM上面跑,这个就是JVM的跨语言特征。

Java SE体系架构

JVM只是一个翻译
JRE提供了基础类库
JDK提供了工具
在这里插入图片描述

二.JVM整体

在这里插入图片描述
Java类加载器ClassLoader把 .class加载到 运行时数据区
运行时数据区:jvm管理的内存
执行引擎把内存中的类进行解释

运行时数据区

在这里插入图片描述
自动化的内存管理

定义: Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
类型: 程序计数器、虚拟机栈、本地方法栈、Java堆、方法区(运行时常量池)、直接内存

白色: 线程私有(隔离)的,在jvm当中运行多线程,假如有3个线程,那么每份线程都有虚拟机栈、本地方法栈、程序计数器
线程共享的: 方法区、堆
方法区: 放class 静态变量 常量
堆: 几乎所有的对象都在堆中分配

1.程序计数器

指向当前线程正在执行的字节码指令的地址

在这里插入图片描述
字节码 code 行号。针对方法体的偏移量
大体认为 ===程序计数器,记录字节码的地址
为什么需要程序计数器:时间片轮转,切出去了 不执行这个了 记录一下执行到哪里了
程序计数器:JVM内存中 唯一不会OOM(out of memory)
程序计数器是很小的一块区域 只需要记录地址,一般需要int类型记录就够了,保存现场

2.虚拟机栈

存储当前线程运行方法所需的数据,指令、返回地址

虚拟机栈:子弹夹
栈帧: 子弹, 一个方法对应一个栈帧
在这里插入图片描述

栈帧:

  1. 局部变量表
  2. 操作数栈
  3. 动态连接
  4. 完成出口

或者: 局部变量表、操作数栈、栈帧信息

虚拟机栈大小是受限制的
大小限制 -Xss 大约(1M)

局部变量表: 存储局部变量,只能存储8大基础数据类型、引用
操作数栈: 存放方法执行、操作

多态:静态分派、动态分派

Person   man /woman
Person  puppy  =  new Man();
puppy.wc();
puppy  =  new woman();
puppy.wc();

编译的时候没法确定上哪个wc
动态连接: 确定该上哪一个 , 符号引用变成具体引用
完成出口(返回地址): 存在每一个栈帧中,栈帧结束后回到哪里,正常返回–程序计数器中地址,异常返回—异常处理表

在这里插入图片描述
局部变量表中需要this: this 是类实例的本身,在非静态的方法里面需要一个this, 非静态方法要通过this才能访问外部
操作数栈就是为了操作(相加…)
方法在栈帧中执行最多的是进出操作数栈

在java中 解释执行 基于操作数栈
c语言基于寄存器
寄存器会快一点(基于硬件) ,缺点 移植行差
Java 兼容性好,效率偏低

3.本地方法栈

本地方法栈保存的是native方法的信息

Native 本地方法 用c语言实现的

当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法
程序计数器不会记录

虚拟机规范无强制规定,各版本虚拟机自由实现
HotSpot直接把本地方法栈和虚拟机栈合二为一

4.方法区

  1. 类信息
  2. 常量
  3. 静态变量
  4. 即时编译期编译后的代码

方法区不同的版本有不同的实现

<=jdk1.7 永久代
先划分一个堆里面新生代和老年代,再以这样的规范画一个永久代
但是一个问题 不单单是回收堆 还会回收永久代(方法区) 但里面都是常量变量什么得,这些回收效率很低,永久代多大受制于堆多大

>=jdk1.8 元空间
元空间可以使用机器内存–不受限制,方便拓展不用受堆的限制抛出OOM, 挤压堆空间

5.Java堆

  1. 对象实例(几乎所有)
  2. 数组

方法区Java堆都是线程共享的,为什么不用一份?而用两个区分
中存放的是对象数组,是需要频繁的回收的
方法区回收的难度大
动静分离的思想,偏静态放到方法区,经常动态创建和回收的放到堆中,便于垃圾回收高效

Java堆的大小参数设置

-Xmx 堆区内存可被分配的最大上限

-Xms 堆区内存初始内存分配的大小

6.直接内存–堆外内存

直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;

如果使用了NIO,这块区域会被频繁使用,在java堆内可以用directByteBuffer对象直接引用并操作;

这块内存不受java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;

在这里插入图片描述

7.从底层深入理解运行时数据区

public class JVMObject {
    public static final String MAN_TYPE = "man";
    public static String WOMAN_TYPE = "woman";

    public JVMObject() {
    }

    public static void main(String[] args) throws Exception {
        Teacher T1 = new Teacher();
        T1.setName("puppy");
        T1.setSexType("man");
        T1.setAge(18);

        for(int i = 0; i < 15; ++i) {
            System.gc();
        }

        Teacher T2 = new Teacher();
        T2.setName("snape");
        T2.setSexType("man");
        T2.setAge(17);
        Thread.sleep(2147483647L);
    }
}
  1. 申请内存
  2. 类加载–class进入方法区
  3. 常量、静态变量 入方法区
  4. 虚拟机栈–入栈帧
  5. 栈帧的方法执行

类JVMObject与Teacher
MAN_TYPE 是JVMObject 里面的常量
WOMAN_TYPE 是JVMObject 里面的静态变量

申请内存
在这里插入图片描述

类加载–class进入方法区
在这里插入图片描述

常量、静态变量 入方法区
在这里插入图片描述
虚拟机栈–入栈帧
虚拟机栈new出来 ,main方法的栈帧进栈
在这里插入图片描述
在堆中new T1, 在局部变量表放入 T1的引用
在这里插入图片描述
循环15次后T1进入老年代,T2被new出来
在这里插入图片描述

F:\javajdkan\jdk1.8.0_202\lib>java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

HSDB工具,监控jvm 内存运行情况,可视化对象找到对象
ps linux查看进程
jps 显示java的进程

23824 HSDB
1252
20740 JVMObject
22744 Launcher
19772 Launcher
6396 Jps

把进程id 20740 绑上去 查看到线程
在这里插入图片描述
虚拟机栈(栈区)
在这里插入图片描述
第一个是sleep(native方法)方法的栈帧,验证了HotSpot直接把本地方法栈和虚拟机栈合二为一。

第二个Interpreted frame jvm/JVMObject.main 是main方法的栈帧
对内存的虚拟化

查看堆里面的对象
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
查看堆的参数:
在这里插入图片描述
新生代划分了eden,from,to,old
T1 puppy对象 在old区
T2 snape对象 在eden区

8.深入辨析堆和栈

功能

以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;

而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;

线程独享还是共享

栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。

堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。

空间大小

栈的内存要远远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError问题。

9.内存溢出

在这里插入图片描述

  1. 栈溢出
  2. 堆溢出
  3. 方法区溢出
  4. 本机直接内存溢出

虚拟机栈中两种异常:StackOverFlow 和 OutofMemory

一直入栈,栈溢出

/**
 * 栈溢出 -Xss  默认1M 顶不住不停入栈
 */
public class StackOverFlow {

    public void puppy(){//一个栈帧--虚拟机栈运行
        puppy();//无穷的递归
    }
    
    public static void main(String[] args)throws Throwable {
        StackOverFlow javaStack = new StackOverFlow(); //new一个对象
        javaStack.puppy();
    }
}
at jvm.StackOverFlow.puppy(StackOverFlow.java:9)

OutofMemory:栈执行抛出
假如1000个线程(虚拟机栈1M),则需要1G同时跑,机器如果只有500M ,则死机OutofMemory,申请时发现没有内存能用了

堆异常

堆的初始和最大设置成30M

/**
 * VM Args:-Xms30m -Xmx30m -XX:+PrintGCDetails
 * 堆内存溢出(直接溢出)
 */
public class HeapOom {
    public static void main(String[] args)
    {

        String[] strings = new String[35*1000*1000];  //35m的数组(堆)
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at jvm.HeapOom.main(HeapOom.java:11)

方法区溢出

cglib动态生成,不断地去编译代码,造成方法区内存溢出,使得方法区限制只有10M

/**
 * cglib动态生成
 * Enhancer中 setSuperClass和setCallback, 设置好了SuperClass后, 可以使用create制作代理对象了
 * 限制方法区的大小导致的内存溢出
 * VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 * */
public class MethodAreaOutOfMemory {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MethodAreaOutOfMemory.TestObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
                    return arg3.invokeSuper(arg0, arg2);
                }
            });
            enhancer.create();
        }
    }

    public static class TestObject {
        private double a = 34.53;
        private Integer b = 9999999;
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

本机直接内存(堆外内存)溢出

/**
 * VM Args:-XX:MaxDirectMemorySize=100m
 * 堆外内存(直接内存溢出)
 */
public class DirectOom {
    public static void main(String[] args) {
        //直接分配128M的直接内存(100M)
        ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1204);
    }
}

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

解决方案:

代码有问题或者内存空间不够
栈发了生死循环
堆原本有很多对象但不够分配
方法区要加载东西太多
堆外内存分配的不够

10.虚拟机优化技术

编译优化技术

方法内联

把目标方法原封不动复制到调用方法中来,避免多一次的方法调用,带来性能的提升
在编译时就能确认是一个表达式,没必要再去调用一次方法,代码执行比方法入栈要快很多

/**
 * 方法内联
 */
public class MethodDeal {

    public static void main(String[] args) {
        //max(1,2);//调用max方法:  虚拟机栈 --入栈(max 栈帧)
        //没必要入一次栈帧
        boolean i1 = 1>2;
    }
    public static boolean max(int a,int b){//方法的执行入栈帧。
        return a>b;
    }
}

栈的优化技术

栈帧之间数据共享
一般情况栈帧是分开的是独立的
如果两个方法之间有数据传递
方法A(局部变量表)–>方法B(操作数栈)
那么区域可以共享

在这里插入图片描述

/**
 * 栈帧之间数据的共享
 */
public class JVMStack {

    public int work(int x) throws Exception{
        int z =(x+5)*10;//局部变量表有
        Thread.sleep(Integer.MAX_VALUE);
        return  z;
    }
    public static void main(String[] args)throws Exception {
        JVMStack jvmStack = new JVMStack();
        jvmStack.work(10);//10  放入main栈帧操作数栈
    }
}

在这里插入图片描述
地址0x00000096 被共享
上面是work栈帧,紫色 局部变量表
下面是main栈帧,红色 操作数栈 expression stack

补充:
JDK1.8 运行时的常量池(字符串部分—放入堆), 静态(class) -----方法区

永久代是方法区在一些版本的实现

方法区的类在什么时候回收?
条件非常严格,同时满足:
1.堆中不存在这个类的任何实例
2.加载该类的垃圾回收器classload已经被回收
3.该类,java.lang.class对象,没有任何地方被引用,其他的类没有引用它,无法通过反射访问该类的方法。
JVM参数控制
-Xnoclassgc(禁止类的垃圾收集)(GC) 不能开启
就可被回收

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值