JVM-内存结构

目录

一、引言

1、什么是JVM,有什么好处?

2、jvm、jre、jdk之间的关系

二、内存结构部分

1、Program Counter Register 程序计数器(寄存器)

 2、虚拟机栈

3、本地方法栈

 4、堆

5、方法区

6、直接内存Direct Memory


一、引言

1、什么是JVM,有什么好处?

JVM:(Java Virtual Machin)  - java程序的运行环境(java二进制字节码的运行环境)

好处:

  • 一次编写到处运行,jvm通过解释的方式执行二进制字节码,实现跨平台运行
  • 自动内存管理机,垃圾回收功能,自动释放内存,减少开发因操作不当造成内存泄漏的问题
  • 数组下标越界检查
  • 多态

2、jvm、jre、jdk之间的关系

  •  jvm是最基本的运行环境
  • jre能够在jvm的基础上提供一些的类库,构成真正意义上的java运行环境
  • jdk则除了运行环境以外,还提供了编译工具,构成了一个开发工具(编译+运行)

二、内存结构部分

1、Program Counter Register 程序计数器(寄存器)

作用:一条代码要通过编译成为二进制字节码文件,然后由解释器变为机器码后才交给cpu运行,而程序计数器就是在二进制字节码中记住下一条jvm的地址,例如下面字节码文件前的编号,可以假设成程序计数器,当0号指令要处理的时候,先交给解释器处理变为机器码,然后交给cpu,而之后就会通过程序计数器找到下一条jvm指令的地址,直接来处理3号指令。程序计数器是线程私有的,在物理层面是通过寄存器来实现的

特点:

  • 程序计数器是线程私有的
  • 物理层方面是通过寄存器来实现,访问速度非常非常的高
  • 不会出现内存溢出

 2、虚拟机栈

概述:栈是线程运行时需要的内存空间,每一个栈都由多个栈帧组成,有先进后出的特点,栈帧的定义是每个方法运行时需要的内存,用于存储机基本数据类型,参数,局部变量,返回地址等,如下图所示、当方法一来时,将方法一的站着放入栈,这时候在方法一中调用方法二、又把方法二的栈帧放入栈,方法三依旧,然后等到方法三先执行完毕后,就将方法三的栈帧移除,然后移除方法二的栈帧、方法一的栈帧。每个线程只能有一个正在活动的栈帧,是处以栈顶部的栈帧,对应为正在执行的方法。

调节栈内存的参数:-Xss

常见面试题

问题一:垃圾回收是否涉及栈内存?

不会,每当方法调用完之后,会自动将栈帧弹出栈,也就是会自动回收内存,不需要垃圾回收来管理

问题二:栈内存分配越大越好吗

不会,由于内存大小是固定的,一个栈消耗的内存越高,那么同时容纳的线程数就会减少,反而使运行速度变慢

问题三:方法内的局部变量是否线程安全

如果没有逃离方法的作用范围,就是线程安全的,因为他们位于不同的栈中,互不干扰

如果方法中变量是返回值,那么其他线程有可能会获取到该引用从而修改,那么就是线程不安全的

方法四:栈内存溢出问题

   如果设置了递归调用方法而不给出合适的结束条件,那么就会不断的往栈中压入栈帧而不释放栈帧,最终会造成栈内存溢出的问题

3、本地方法栈

概述:java虚拟机调用一些本地方法(Native Method)时,需要给这些本地方法提供的内存空间。

本地方法:本地方法是指不是由java代码编写的方法,因为java代码具有一定的限制,有的时候不能去直接跟操作系统底层打交道,所以就需要这种用c或者c++语言编写的本地方法来与操作系统更底层的api接口打交道。例如Object类中的方法就是本地方法,clone、notify、notifyall、wait等

 4、堆

概述:Java中几乎所有的实体对象都存放在堆中,堆也是垃圾回收器所管理的区域,所以一些资料中也会称之为GC堆,堆内存是线程共享的,要考虑线程安全问题,并且由垃圾回收机制

堆内存溢出问题:

不断地创建新的对象并且在使用对象,造成堆内存占用越来越多,最终导致堆内存溢出

调节堆内存的参数:-Xmx

5、方法区

概述:根据官方文档定义,方法区是Java虚拟机中所有线程共享的区、这一点跟堆类似,里面存储着跟类的结构相关的信息(成员变量、方法数据、成员方法代码、构造器方法代码、特殊方法(类的构造器))、运行常量池。方法区在虚拟机启动时被创建,逻辑上是堆的组成部分(但是实现上不一定会遵从逻辑上的定义)

Hotspot虚拟机在1.6和1.8的方法区实现(1.7属于过度阶段):

 在1.6中方法区使用永久代实现,和堆的内存是相连的,由虚拟机管理内存。

1.6以前,字符串常量池、静态变量存储,类信息在方法区永久代之中,在jdk1.8以后存储在元空间中,静态变量存放在堆空间中。

在1.8中方法去由元空间实现,从虚拟机中迁移到本地内存,不由虚拟机管理内存空间,但是StringTable则放在堆中

方法区内存溢出

1.8 之前会导致永久代内存溢出

使用 -XX:MaxPermSize=8m 指定永久代内存大小

1.8 之后会导致元空间内存溢出

使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

常量池:

常量池实际是就是一张表,虚拟机根据这张表去找到要执行的类名、方法名、参数类型、字面量等信息

先看看一个类以及反编译后的代码:

源代码:

public class DeadLockDemo {
    public DeadLockDemo() {
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("hellow word");
    }
}

反编译后的代码:

Classfile /D:/study/target/classes/DeadLockDemo.class
  Last modified 2021-11-17; size 733 bytes
  MD5 checksum 912386becce8b221202b8c8a23268131
  Compiled from "DeadLockDemo.java"
public class DeadLockDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:(常量池)
   #1 = Methodref          #6.#25         // java/lang/Object."<init>":()V
   #2 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #28            // hellow word
   #4 = Methodref          #29.#30        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #31            // DeadLockDemo
   #6 = Class              #32            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LDeadLockDemo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               Exceptions
  #19 = Class              #33            // java/util/concurrent/ExecutionException
  #20 = Class              #34            // java/lang/InterruptedException
  #21 = Utf8               SourceFile
  #22 = Utf8               DeadLockDemo.java
  #23 = Utf8               RuntimeVisibleAnnotations
  #24 = Utf8               Ljdk/nashorn/internal/runtime/logging/Logger;
  #25 = NameAndType        #7:#8          // "<init>":()V
  #26 = Class              #35            // java/lang/System
  #27 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #28 = Utf8               hellow word
  #29 = Class              #38            // java/io/PrintStream
  #30 = NameAndType        #39:#40        // println:(Ljava/lang/String;)V
  #31 = Utf8               DeadLockDemo
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/util/concurrent/ExecutionException
  #34 = Utf8               java/lang/InterruptedException
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (Ljava/lang/String;)V
{
  public DeadLockDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDeadLockDemo;

  public static void main(java.lang.String[]) throws java.util.concurrent.ExecutionException, java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hellow word
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
    Exceptions:
      throws java.util.concurrent.ExecutionException, java.lang.InterruptedException
}
SourceFile: "DeadLockDemo.java"
RuntimeVisibleAnnotations:
  0: #24()

不难发现虚拟机要执行的指令为:

 而每条指令后面都有一个#加数字,代表要从常量池中去找常量符号,而常量池就是Constant pool里的内容(下图所示)。正因为有了常量池才能实现虚拟机指令的执行,

运行时常量池

常量池是*.class文件中的,当类被加载的时候,他的常量信息就会放入运行时常量池,并把里面的符号地址(例如 #1#2#3...)变为真实地址。

StringTable特性:

  • 常量池中的字符串仅仅是符号,只有在第一次用到的时候才会变为对象,存入串池(StringTable)中
  • 利用串池的机制避免重复创建字符串对象
  • 字符串变量的拼接原理是Stringbuilder(1.8)
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern()方法主动将串池中还没有的字符串放入串池
  • 在1.6中串池位于永久代中(方法区),在1.8中串池位于堆空间中

StringTable的垃圾回收

JVM相关参数

  • -Xmx10m 指定堆内存大小
  • -XX:+PrintStringTableStatistics 打印字符串常量池信息
  • -XX:+PrintGCDetails
  • -verbose:gc 打印 gc 的次数,耗费时间等信息

-XX:+UseGCOverheadLimit:  前面符号是+表示打开
如果百分之98的时间都花费在垃圾回收但是上,只有百分之二的堆空间被回收,那么jvm就已经到了不可救药的地步,这时候就不会在尝试进行垃圾回收

StringTable调优

1、如果项目中涉及的字符串比较多,可以通过字符串入池来减少字符串对象的个数,节约内存空间

2、由于StringTable的底层结构式HashTable,适当增加桶的个数可以减少遍历的时间,将字符串放入串池中的速度明细能够提高

调优参数:-XX:StringTableSize=桶个数(默认值是6万,最小值是1009 )

intern()

jdk1.8

在jdk1.8中字符串调用intern可以尝试将字符串放入串池,如果成功则返回串池中的字符串

例如:

 String a = "a";    //往串池里放入"a"
        String b = "b";     //往串池里放入"b"
        String ab = a+b;       //new 一个String 对象"ab"放入堆中
        String x =  ab.intern();   //尝试将”ab“放入串池,如果串池中没有,就将自己放入串词,此时变量ab指向的是串池中的"ab"
        System.out.println(ab=="ab");//true
        System.out.println(x=="ab");// true

jdk1.6

字符串调用intern可以尝试将字符串放入串池,但是不是将本身放进去,而是复制一个新的字符串放入串池中,如果成功则返回串池中的字符串

 String a = "a";   //将"a"放入串池
        String b = "b"; //将"b"放入串池
        String ab = a+b;    //new 一个 String 对象 "ab" 放入堆中
        String x =  ab.intern();  //判断串池中有没有"ab" 如果没有则复制一份堆中的"ab" 放入串池中 并返回串池中的"ab"
        System.out.println(ab=="ab");//此时ab变量仍然指向堆空间    false
        System.out.println(x=="ab");//true

6、直接内存Direct Memory

  • 常见于NIO操作时,用于数据缓冲
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

为什么使用直接内存的读取性能会高呢?

从文件读写来看,先看一下传统io,如下图

 涉及到磁盘读取的时候会由底层操作系统进行操作,由用户态变为内核态。在内存方面,当切换到内核态的时候,就会由底层函数读取磁盘里的内容,然后再系统内存中划出一块缓存区,磁盘的内容就会读入系统缓存区中,但是系统缓存区java代码是不能访问的,java会在堆内存中分出一块缓存区,这时候才有java进行其他操作,由于有两块缓冲区,那么数据就会涉及到读两次,造成了不必要的数据复制,效率无形之中降低了

现在看一下NIO的过程:(如下图)

当调用ByteBuffer.allocateDirect()的时候就会再操作系统上分配一块直接内存,操作系统划出的这一块内存java代码也可以访问,属于共享内存区。磁盘文件读取到直接内存后java代码就可以直接通过直接内存拿到数据,效率无形之中就得到了提升

禁用显示垃圾回收:

因为显示垃圾回收(System.gc();)是一种Full gc,性能十分低下,为了避免程序员在某些地方不小心使用了显示回收,有必要再虚拟机参数禁用显示回收

禁用方法:-XX:+DisableExplicitGC  // 静止显示的 GC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值