程序计数器
定义
程序计数器(Program Counter Register)
作用
Java源代码被编译为二进制字节码后,源代码会被翻译为JVM指令(JVM指令就是跨平台的依据)。这些JVM指令CPU是不能直接执行的,需要由解释器(就是执行引擎中的解释器)翻译为CPU可以看懂的机器码然后执行。
程序计数器,可以看作是当前线程所执行的字节码的行号指示器。
解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令(JVM指令)。
在物理上程序计数器的实现是由寄存器实现的。
特点
- 是一块内存较小的区域
- 线程私有,各条线程止线计数器互不影响。随线程创建而创建,线程销毁而销毁。
- 程序计数器的内存区域是《Java虚拟器规范》中唯一一个没有规定任何OutOfMemoryError(内存溢出)情况的区域。
Java虚拟机栈
定义
Java Virtual Machine Stacks(Java虚拟机栈)
- 每个线程运行时需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Stack Frame)组成,对应每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的的哪个方法
问题
- 垃圾回收是否涉及栈内存?不涉及,垃圾回收只涉及到堆,栈内存不会也不需要垃圾回收
- 栈内存分配越大越好吗?并不是,栈变大每个线程需要的内存会变多,而物理内存是固定的,导致线程数量减少。
- -Xss size 用于指定栈内存
- linux/x64(64-bit):1024kb
- macOS(64-bit):1024kb
- 方法内的局部变量是否线程安全?不一定,看这个局部变量是否是线程私有的。也就是说这个变量如果是存储在栈帧中就是线程安全的,若是存储在堆内的对象,则需要考虑线程安全问题。具体还要看对象的发布与溢出状况。
栈内存溢出-StackOverflowError
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
Java内存区域异常
Java虚拟机规范中规定了两种异常状况:一是上标题介绍的StackOverflowError;在栈中如果栈大小可以动态扩展(有些虚拟机可以)若无法申请到足够内存时会抛出OutOfMemoryError异常。
案例-线程运行诊断
案例1:cpu占用过多
三个命令:top、ps、jstack
linux下top命令可以查看CPU占用情况,可以找到有问题的进程编号
ps命令可以查看线程对CPU的占用情况ps H -eo pid,tid,%cpu | group 进程编号
-eo代表要展示什么信息,| grop表示通过进程编号分组
jstack 进程编号,命令可以展示该进程所有线程中的详细信息,注意对应的线程编号jstack命令中展示的是16进制的需要转化一下查看
案例2:程序运行很久没有结果
可以直接使用jstack命令查看,可以定位死锁。
本地方法栈
本地方法栈(Native Method Stacks)。它和虚拟机栈发挥的作用非常相似。其本质区别就是虚拟机栈为Java方法服务而本地方法栈为本地(native)方法服务。
例如Object类的hashCode方法wite方法都是使用native修饰的本地方法。
堆
定义&特点
Java堆(Heap)是虚拟机管理的内存中最大的一块。Java堆是被所有线程共享的内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
通过new关键字创建的对象都会使用堆内存。
堆内存有垃圾回收机制。也可称为GC堆。
现代垃圾收集器都是基于分带收集理论设计的,所以会出现以下名词:新生代、老年代、永久代、Eden空间、FromSurvivor空间、To Survivor空间。
Java堆可以处于物理上不连续的内存空间中,但在逻辑上应该被视为连续的。
Java堆既可以被实现成固定大小的,也可以是扩展的。不过当前主流的Java虚拟机的实现都是按照可扩展来的。
堆内存溢出
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时就会抛出OutOfMemoryError异常。
小tips:并没有任何情况表明栈只能抛出StackOverflowError和堆只能抛出OutOfMemoryError。它们4者之间并没有绑定关系。
堆内存诊断
- jps工具:查看当前系统中有哪些java进程
- jmap工具:查看堆内存占用情况
- jconsole工具:图形界面,多功能的检测工具,可以连续监测
方法区
定义&特点
方法区(Method Area)与Java堆一样是各个线程共享的内存区域。它用于存储已被虚拟机加载的类型信息、常量、静态变量等数据。可以把方法区理解为堆的一个逻辑部分。
在Java 8之前方法区是使用永久代(Permanent Generation)来实现的。
由于永久代实现方法区有些许缺陷。在Java 6 ~ Java 7时开发人员就开始考虑修改方法区的实现方案了。直到Java 8完全放弃了永久代的概念改用在本地内存(Native Memory)中来实现元空间(Meta space)代替。
方法区内存溢出
若方法区无法满足心得内存分配需求时,将抛出OutOfMemoryError异常。
Java 8以前是永久代溢出
-XX:MaxpermSize=8m // 用来设置最大永久代大小
Java 8以后是元空间溢出
-XX:MaxMetasppaceSize=8m // 用来设置最大元空间的大小
两个内存溢出抛出的异常都是OutOfMemoryError异常,但是提示信息会不同。
运行时常量池
运行时常量池&常量池
区分运行时常量池(Runtime Constant Pool)和常量池(Constant Pool)。
Class文件中除了有类的版本、字段、方法、接口等信息外,还存在一个常量池表(Constant Pool Table)存放各种字面量和符号引用。虚拟机就是根据这张表来找到要执行的类名、方法名、参数类型、字面量等信息。
运行时常量池是方法区的一部分,注意常量池是class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
字符串常量池
介绍
字符串常量池(StringTable)。StringTable是一个Hash表。
通过双引号定义的字符串就会放在StringPool中,而通过new String()创建的字符串则会放在堆中。
通过双引号定义的字符串,在类被加载时会被放入运行时常量池中,注意此时它还并不是java对象,仅为一些符号保存在运行时常量池中。只有当代码执行到它所在的行时才会被创建并加入字符串常量池中。也就是懒惰性。
如果一个字符串存在于字符串常量池中,当执行到需要它的时候就会直接从常量池中获取。只有不存在时才会添加到常量池中。
字符串常量池可以被垃圾回收。
由于StringTable的实现是一个哈希表,所以可以通过提高哈希表的桶子个数来增加StringTable的行能。
-XX:StringTableSize=200000 取值范围再1009 到 2305843009213693951之间
字符串拼接
字符串的拼接可以分为两种,一是字符常量的拼接,二是字符变量的拼接
而编译器对两者都有优化
String a = "a" + "b"; // 常量的拼接
String b = "b";
String c = "c";
String d = b + c; // 变量的拼接
对于变量的拼接在Java基础中就有所涉猎,编译器会使用StringBuild进行字符串拼接(1.8),编译后的代码为:
String b = "b";
String c = "c";
(new StringBuilder()).append(b).append(c).toString();
StringBuilder的toString方法会使用new创建一个新的String。故变量的拼接完成后生成的字符串是直接放在堆中的。
对于常量的拼接,编译器会认为它的结果是确定的就会直接得出结果例如:
String a = "a" + "b";
// 编译后
String a = "ab";
这里的结果"ab"就会放入字符串常量池中。
位置
Java 6 中方法区的实现是永久代,字符串常量池就是在永久代中。
Java 8 之后方法区使用元空间实现,且字符串常量池改为放在堆中,不在存在于方法区中。
主动放入字符串常量池
可以使用intern方法将字符串放入字符串常量池。
在Java 6若intern方法放入的字符串在字符串常量池中不存在,就会将该字符串的复制一份,将复制体放入字符串常量池。若存在则不会入池。不论存在与否都会返回字符串常量池中的该字符串。
在Java 8若intern方法放入的字符串在字符串常量池中不存在,就会直接将该字符串放入字符串常量池。若存在则不会入池。不论存在与否都会返回字符串常量池中的该字符串。
课外练习
第一题
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "a" + "b";
String s5 = s1 + s2;
String s6 = s5.intern();
// 问:
s3 == s4; // true
s3 == s5; // false
s3 == s6; // true
解析:
s3执行时把"ab"放入字符串常量池中,s4会被优化为"ab"直接从常量池中取到"ab",故s3 == s4
s5是变量的字符串拼接最后结果是StringBuild的toString方法new出来的字符串放在堆里的,故而s3 != s4
s6是把s5代表的"ab"入池但是池中已经有了"ab"了直接返回,故而s3 == s6
第二题
String str = new String("a") + new String("b");
// 问上代码一共创建了多少个对象 答:6
首先"a"和"b"分别创建了一个对象放入了字符串常量池,两个new关键字创建了两个String对象放入了堆,对于变量的字符串拼接会使用StringBuild所以又是一个对象,而StringBuild的toString方法会生成新的String放入堆。故而一共创建了6个对象。
直接内存
介绍&特点
直接内存(Direct Memory)就是机器的本地内存。
- 不属于JVM,也不受JVM内存回收管理
- 常见NIO(New Input Ouput)操作,用于缓冲区
- 分配和回收成本比较高,但是读写性能高
扩展
直接内存虽然不受JVM管理,但是Java可以使用Unsafe类来分配和释放的直接内存。但是不建议普通程序员使用,一般都是由JDK内部进行使用。
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freMemory来释放直接内存。
-XX:+DisableExplicitGC 含义为禁止显示的垃圾回收
例如代码中的System.gc()-显示的垃圾回收 Full GC
如果禁用了显式的GC对于我们系统中使用显式GC回收直接内存就会无效,导致直接内存无法回收。可以直接使用Unsafe类来手动的管理直接内存