JVM学习笔记(一)

内存结构

程序计数器

1)定义

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

作用:是记录下一条 jvm 指令的执行地址行号。

特点:

是线程私有的

【Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的(时间片轮转),在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了形成切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各计数器之间互不影响,独立存储

不会存在内存溢出——这是唯一一个在java虚拟机规范中没有规定任何内存溢出情况的区域

【如果执行的java方法,计数器里就是指令地址,如果执行的是native方法(一个Native Method就是一个java调用非java代码的接口,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数),计数器值为空(undefined)】

2)作用

左边:二进制字节码 JVM指令(linux,windows,都长这样) 左边:JAVA源代码

0: getstatic #20 // PrintStream out = System.out; 
3: astore_1 // -- 
4: aload_1 // out.println(1); 
5: iconst_1 // -- 
6: invokevirtual #26 // -- 
9: aload_1 // out.println(2); 
10: iconst_2 // -- 
11: invokevirtual #26 // -- 
14: aload_1 // out.println(3); 
15: iconst_3 // -- 
16: invokevirtual #26 // -- 
19: aload_1 // out.println(4); 
20: iconst_4 // -- 
21: invokevirtual #26 // -- 
24: aload_1 // out.println(5); 
25: iconst_5 // -- 
26: invokevirtual #26 // -- 
29: return

但是JVM指令(字节码)还不够,还要交给解释器,翻译成机器码,才能交给CPU使用。CPU只认识机器码

  • 解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行

你可以将程序计数器看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

计数器实现是通过寄存器实现的

  • 多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

经常有人把Java内存区域笼统地划分为堆内存(Heap)和栈内存(Stack),这种划分方式直接继承自传统的C、C++程序的内存布局结构,在Java语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。不过这种划分方式的流行也间接说明了程序员最关注的、与对象内存分配关系最密切的区域是“堆”和“栈”两块。其中,“堆”在稍后笔者会专门讲述,而“栈”通常就是指这里讲的虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

每个线程运行需要的内存空间,称为虚拟机栈

每个栈由多个栈帧(Frame)组成(就是压入栈的一块块),对应着每次调用方法时所占用的内存(比如参数,局部变量,返回地址等)

调用一个方法,就把对应栈帧压入栈,调用完了弹栈

栈中有多个栈帧是可能的:方法1调用方法2,方法2调用方法3,方法3调用方法4……

每个线程只能有一个活动栈帧,对应着当前正在执行的方法,一直都在栈的顶部

问题辨析:

  • 垃圾回收是否涉及栈内存?

不会。栈内存是方法调用时产生的栈帧内存,方法调用结束后会弹出栈,栈帧内存自动地就回收了,所以不需要垃圾回收来管理。

  • 栈内存分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,只是说可以支持更多的递归调用,但是可执行的线程数就会越少。比如总共有500m,一个栈内存1M,总共最多能有500个线程同时运行。但是假如栈内存有10m,那最多能有509个线程同时运行了

  • 方法内的局部变量是否线程安全

要看是不是线程安全的,就看这个变量对多个线程是公有的,还是每个线程私有的

如果方法内部的变量没有逃离方法的作用范围,它是线程安全的

如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。(如变量是参数,函数返回值等等)

2)栈内存溢出

栈帧过大(比较少见)、过多(典型:递归)、或者第三方类库操作(如产生了循环依赖,导致库一直在add),都有可能造成栈内存溢出 java.lang.stackOverflowError ——这是栈内存溢出报的错,使用 -Xss256k 指定栈内存大小!

本地方法栈

java调用本地方法时,给本地方法提供的内存空间

native方法,就是不是右java编写的方法。因为 JAVA 有时候没法直接和操作系统底层交互,所以need JAVA 去调用本地的C或者C++方法去和底层API打交道

所以需要用到本地方法栈,服务于带 native 关键字的方法。

本地方法栈还是很多的,比如object中的clone方法,可以看到没有实现代码,其实就是调用cc++的实现接口

还有hashcode,wait方法等等……很多

1)定义

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配” 不过随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换[2]优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。但是老年代新生代这些,只是一部分垃圾收集器的共同特性,不是固定的。java堆没有必须划分成“新生代 老年代”的说法——尤其是现在有不采用分代设计的新垃圾收集器了

如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区

(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

Heap 堆

通过new关键字创建的对象都会被放在堆内存

特点

它是线程共享,堆内存中的对象都需要考虑线程安全问题

有垃圾回收机制,对象不被使用时就回收掉

2)堆内存溢出

如果不断地产生对象,且对象一直在使用得不到释放,就会耗尽堆内存,产生溢出

方法区

1)定义

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。

《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。

这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!

方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。

方法区是规范,永久代(jdk1.6,占用的是堆内存)和元空间(jdk1.8 ,占用的是系统的内存)是实现。

4)运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)

首先放结论:

常量池:

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

运行时常量池:

常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址(把#1 #2这些变成真实的地址)

5)StringTable(串池)

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

intern方法 1.8

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

intern方法 1.6

  • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
  • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

区别就是常量池中串的地址不一样,成功放入时,1.7以前放入的是新的地址,1.7开始放入的是堆中s的地址

6)StringTable 的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

永久代只有fullGC的时候才会垃圾回收,回收效率不高,会占用大量内存

所以放入堆中,minorGC 的时候就开始垃圾回收了

在jdk8下设-Xmx16m(堆内存) -XX:-UseGcoverheadLimit(这个默认开了,意思是jvm用98%的时间进行垃圾回收,但是只回收了2%的空间,这时候就没救了,报这个异常)把这个关了,就能看到报堆内存不足的错了

在jdk6下设-XX : MaxPermSize=16m(永久代大小)

来更改串池大小

直接内存

1)定义

Direct Memory

它是操作系统中的一块区域,不是JVM中的一块区域

  • 常见于 NIO 操作时,用于数据缓冲区

【java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。NIO三大组件:Channel、Buffer、Selector】

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

2)使用直接内存的好处

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件。会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后再将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值