目录
1、Program Counter Register 程序计数器(寄存器)
一、引言
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