01 JVM的内存结构
1 JVM的基础知识
1-1 概述
定义:Java Vritual Machine是Java程序的运行环境(Java二进制字节码的运行环境)
好处:
- 一次编写,到处运行(跨平台)
- 自动的内存管理,垃圾回收功能。
- 数组下标越界检查(会抛异常,避免数据损失)
- 多态(使用虚方法表的机制)
1-2 比较JVM和JRE和JDK
JVM:Java virtual machine
JRE: Java Runtime Environment
JDK: Java development Kit
Java SE (Java Platform,Standard Edition): 桌面应用。
Java EE (Java Platform,Enterprise Edition): web应用
Java ME (Java Platform,Micro Edition):手机/机顶盒等应用
1-3 常见的JVM实现
注意:
- JVM是一套规范,只要遵循这个规范,可以自己开发一套JVM实现。
- 本文档所有关于JVM都是HotSpot实现
1-4 JVM的知识点划分
JVM整体可以大致分为三块分别是classLoader、JVM的内存结构、执行引擎:
- 类加载器:Java源代码转换为字节码后必须经过类加载器才能进入到JVM运行
- 内存结构:
- 方法区:创建的类(class)就放入方法区
- Heap:由类创建的实例(对象)放在堆当中,实例在调用方法时会使用虚拟机栈、程序技术器、本地方法栈。
- 执行引擎:
- 方法在执行时会使用解释器模块,方法中的热点代码(频繁调用代码)会通过即使编译器模块优化执行。
- GC模块用于回收那些不再被引用的对象
- 本地接口方法:所谓本地是指虚拟机所依托的操作系统,调用操作系统接口提供的方法。
学习路线:JVM内存结构---->垃圾回收机制---->Java class层面的优化-------->Java的类加载器----->JIT(Just In Time),Java即时编译器
2 JVM的内存结构
2-1 Java中程序计数器
本质:CPU的寄存器(Program counter Register:PCR)
作用:存放下一条JVM指令的执行地址
特点:
- 程序计数器是线程私有的(很容易理解,每个线程时间片用完,肯定需要PCR保存当前执行的位置)
- 程序计数器没有内存溢出
2-2 Java中的虚拟机栈(JVM Stacks)
虚拟机栈:Java每个线程执行时所需要的内存空间
- Java的每一个线程在执行后会被分配一个栈内存。
- 栈内存由多个栈帧组成,每个栈帧对应一次方法调用
- 在栈顶部是当前线程的活动栈帧。
虚拟机栈中的栈帧:线程中每个方法运行时需要的内存空间
- 栈帧中存储每个方法的参数,局部变量以及返回地址。
2-2-1 与栈帧相关的经典问题辨析(重要)
1.垃圾回收是否涉及栈内存?
不涉及栈内存,栈内存是线程是私有的内存,垃圾回收的的对象是堆中不再引用的实例。
2.栈内存分配越大越好吗?
栈内存可以通过虚拟机的参数设置,栈内存过大会导致2方面影响:
- 单台机器并发的线程数目降低
- 单个线程方法调用的次数上限变高
3.方法内局部变量的是否线程安全?
需要考虑方法内变量是否逃离该方法的作用范围:
- 局部变量只在方法内部被使用,那么变量存储在某个栈帧中,被该线程私有。
- 局部变量引用传入的参数,或者作为参数返回,那么可能存在局部变量的线程安全问题。(见下面代码示例)
局部变量线程安全分析实例
- 下面代码中可以考虑用线程安全的类StringBuffer替代StringBuilder.
package part1;
public class Test1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
/*情况1:局部变量sb没有逃离方法作用范围,局部变量线程安全*/
public static void m1(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
/*情况2:局部变量sb逃离方法作用范围,引用外部传入的参数,局部变量线程存在安全隐患*/
/*局部变量sb可能会被其他线程访问*/
public static void m2(StringBuilder sb){
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
/*情况3:局部变量sb逃离方法作用范围,作为返回值,局部变量线程存在安全隐患*/
/*局部变量sb可能会被其他线程访问*/
public static StringBuilder m3(){
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
return sb;
}
}
2-2-2 栈内存溢出问题(开发过程的常见问题)(重要)
发生的场景:
- 栈帧过多(方法递归调用)导致栈内存溢出
- 栈帧过大导致栈内存溢出
总结: 确保:线程的栈内存空间 >= 方法递归调用次数* 每个方法存储的局部变量大小。
体会:使用C++做算法题过程中,设计递归函数,vector
- 方式1: 每递归调用一次,对vector对象进行拷贝,造成每个栈帧中都有一个vector对象。如果递归调用次数多并且vector中存放的数据较大会造成爆栈。
- 方式2:只传递引用,特别是对vector对象数组只进行读操作时。C++内存结构
栈内存溢出实例1:
package part1;
public class Test2 {
private static int count = 0;
public static void main(String[] args) {
try{
method1();
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(count);
}
}
private static void method1(){
count++;
method1();
}
}
执行结果(调用了23436次,主线程的栈内存不够了):
23436
Exception in thread "main" java.lang.StackOverflowError
at part1.Test2.method1(Test2.java:16)
.....
修改jvm的栈内存为256KB后执行结果:
3604
Exception in thread "main" java.lang.StackOverflowError
at part1.Test2.method1(Test2.java:15)
.....
栈内存溢出实例2(不恰当使用库,循环依赖):
package part1;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
/**
* json 数据转换
*/
public class Test3 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
// 调用提供的方法将实体类转换为json数据
// 但由于Emp表与Dept表存在相互的应用的问题,会造成转换过程无法终止
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
// @JsonIgnore
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
} public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
去除@JsonIgnore注解部分运行结果:
- 可以看到infinite recursion(StackOverflowError)的错误
- 由于代码中2个表的实体类中都包含有对方的引用,循环转换造成json格式转化出现这样的问题
- 加上@JsonIgnore使得转化时忽略对方的引用
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: part1.Dept["emps"]->java.util.Arrays$ArrayList[0]->part1.Emp["dept"]->part1.Dept["emps"]->java.util.Arrays$ArrayList[0]->part1.Emp["dept"]->part1.Dept["emps"]->java.util.Arrays$ArrayList[0]->part1.Emp["dept"]->part1.Dept["emps"]->java.util.Arrays$ArrayList[0]->part1.Emp["dept"]->part1.Dept["emps"]->java.util.Arrays$ArrayList[0]->part1.Emp["dept"]->part1.Dept["emps"]-
.........
2-3 Java线程运行诊断方法
如何定位Java进程中某个线程CPU占用过高?
step1:用top方法定位那个进程占用CPU过高。
step2: 使用下列命令进一步定位Java进程中哪个线程占用过高,获取具体的线程id
- ps (process status)命令用于显示当前进程的状态,类似于 windows 的任务管理器。
ps -H -eo pid,tid,%cpu | grep 进程id
step3: 使用 jstack 进程id 查看Java进程所包含的线程的具体信息,锁定step2获取的线程id
- 利用信息查看程序代码出错位置
线程死锁的定位?
- 利用jstack
- 利用jconsole
2-4 本地方法栈(native)
本地方法栈:本地方法运行所需的内存空间
-
一个Native Method就是一个java调用非java代码的接口
-
该方法的底层实现由非Java语言实现,比如C。这个特征并非java特有,很多其他的编程语言都有这一机制,比如在C++ 中,你可以用extern “C” 告知C++ 编译器去调用一个C的函数。
-
在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。本地接口的作用是融合不同的编程语言为java所用,它的初衷是融合C/C++程序。标识符native可以与其他所有的java标识符连用,但是abstract除外。
实例:下面Object对象中使用natvie修饰的方法就是本地方法。
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
2-5 Java中的堆
2-5-1 堆的概述
通过new关键字创建的对象都属于堆内存。
特点:
- 堆是线程共享的,堆中的对象需要考虑线程安全问题
- 堆受垃圾回收机制管辖。
2-5-2 堆内存不足
测试代码
package part1;
import java.util.*;
public class Test4 {
public static void main(String[] args) {
int i = 0;
try{
List<String> list = new ArrayList<String>();
String a = "hello";
while(true){
list.add(a);
a = a + a;
i++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
}
执行结果:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at part1.Test4.main(Test4.java:11)
25
2-5-2 堆内存诊断的工具
JPS(Java Virtual Machine Process Status Tool)工具:查看进程状态
JMAP(Java Virtual Machine Process Status Tool)工具:生成虚拟机的内存转储快照(heapdump)文件,连续检测不太方便。ss
JCONSOLE工具:图形界面,多功能工具,非常强大。可连续检测。
特点:前面2个采用命令行的方式执行,值得注意的是JPS和JCONSOLE也是用于检测死锁的常用工具。其中JPS配合JSTACK查看线程状态。
测试程序
package part1;
public class test5 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(10000);
byte[] array = new byte[1024*1024*10]; // 10Mb
System.out.println("2....");
Thread.sleep(20000);
array = null;
System.gc();;
System.out.println("3..");
Thread.sleep(30000);java
}
}
方式1: jps结合jconsole命令
C:\Users\Administrator>jps
16064 Jps
18644 Launcher
19316 test5
7816
15884 Launcher
/*****************************************打印2之后使用jmap命令***************************************************************/
C:\Users\Administrator>jmap -heap 19316
Attaching to process ID 19316, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
using thread-local object allocation.
Parallel GC with 4 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2122317824 (2024.0MB)
NewSize = 44564480 (42.5MB)
MaxNewSize = 707264512 (674.5MB)
OldSize = 89653248 (85.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 34078720 (32.5MB)
used = 15274096 (14.566513061523438MB)
free = 18804624 (17.933486938476562MB)
44.82004018930289% used
From Space:
capacity = 5242880 (5.0MB)
used = 0 (0.0MB)
free = 5242880 (5.0MB)
0.0% used
To Space:
capacity = 5242880 (5.0MB)
used = 0 (0.0MB)
free = 5242880 (5.0MB)
0.0% used
PS Old Generation
capacity = 89653248 (85.5MB)
used = 0 (0.0MB)
free = 89653248 (85.5MB)
0.0% used
3197 interned Strings occupying 261568 bytes.
/*****************************************打印3之后使用jmap命令***************************************************************/
C:\Users\Administrator>jmap -heap 19316
Attaching to process ID 19316, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
using thread-local object allocation.
Parallel GC with 4 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2122317824 (2024.0MB)
NewSize = 44564480 (42.5MB)
MaxNewSize = 707264512 (674.5MB)
OldSize = 89653248 (85.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 34078720 (32.5MB)
used = 681592 (0.6500167846679688MB)
free = 33397128 (31.84998321533203MB)
2.0000516451322117% used
From Space:
capacity = 5242880 (5.0MB)
used = 0 (0.0MB)
free = 5242880 (5.0MB)
0.0% used
To Space:
capacity = 5242880 (5.0MB)
used = 0 (0.0MB)
free = 5242880 (5.0MB)
0.0% used
PS Old Generation
capacity = 89653248 (85.5MB)
used = 1053168 (1.0043792724609375MB)
free = 88600080 (84.49562072753906MB)
1.1747125993695176% used
3183 interned Strings occupying 260576 bytes.
方式2:使用jconsole工具
2-5-3 场景分析:一个程序在执行多次垃圾回收机制后,内存占有依旧很高,采用什么工具分析这个进程内存占有过高的原因?
原因:编程原因,导致程序中一些不是必须的引用没有释放。
如何定位程序中哪些对象内存占用比较大?
- 使用jvisualvm工具
2-6 Java的方法区
2-6-1 方法区定义
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
The following exceptional condition is associated with the method area:
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
- 方法区被Java虚拟机所有线程共享
- 方法区存储内容
- 每个类的结构(常量池,成员变量(field),方法数据)
- 成员方法以及构造方法的代码
- 方法区位置:
-
方法区的位置根据JDK版本以及各个厂商对于JVM的实现会存在不同。
-
方法区的实现比如永久带(位于堆内存)以及元空间(不属于堆内存)都是一种实现。
-
方法区是JVM的规范:1)jdk6中对方法区的称之为永久带。 2) jdk8中对方法区的实现称为元空间。
-
-
- 方法区如果内存溢出,会抛出OutOfMemoryError.
- 在JDK1.6实现中,方法区通过永久代(概念层面)实现。
- 在JDK1.8中废弃了永久代,采用元空间实现了方法区。值得注意的是StringTable被移动到heap堆中。
2-6-2 方法区内存溢出
通过虚拟机参数设置元空间大小为8m.
测试代码
package part1;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
// classLoader用于加载类的二进制字节码
// 方法区内存溢出测试原理:类加载的个数过多,导致方法区内存溢出。
// -XX:MaxPermSize=8m 测试永久代的虚拟机参数JDK1.6
// -XX:MaxMetaspaceSize=8m 测试元空间的虚拟机参数JDK1.8
// P25
public class test6 extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
// classWriter用于生成二进制字节码
test6 test = new test6();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter cw = new ClassWriter(0);
/*vist()参数说明:visit(类的版本号,类的访问权限,类的名称,包名,父类,类实现的接口)*/
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 将加载的类二进制字节码存储到byte数组中
byte[] code = cw.toByteArray();
// defineClass执行类的加载,不会进行后续的链接等其他步骤
// 参数说明:(类的名称,字节码数组,偏移量,字节码的长度)
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
执行结果
5346
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at part1.test6.main(test6.java:25)
- 可以看到对于JDK1.8,方法区所对应的元空间产生内存溢出。
2-6-3 方法区内存溢出的场景
CGLIB(code generation library): It is a byte instrumentation library used in many Java frameworks such as Hibernate or Spring. The bytecode instrumentation allows manipulating or creating classes after the compilation phase of a program.
为什么要考虑方法区的溢出?
- spring和mybatis会用到字节码工具,spring利用CGLIB生成代理类。动态加载类与使用类的框架可能会大量生成大量的类。
2-6-4 方法的常量池(class文件的常量表)
代码
package part1;
public class test7 {
public static void main(String[] args) {
System.out.println("hello");
}
}
命令
javap -v test7
通过java工具阅读二进制的class信息:
Last modified 2021-4-2; size 524 bytes
MD5 checksum 007ad80a09c5a34e2e1cc9e6b7a4d331 // 签名
Compiled from "test7.java"
public class part1.test7
minor version: 0
major version: 52 // 內部版本号
flags: ACC_PUBLIC, ACC_SUPER // 类的访问修饰符
/*====================类的常量池========================================================*/
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // part1/test7
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lpart1/test7;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 test7.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 part1/test7
#27 = Utf8 java/lang/Object //
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
/*============类的方法定义====================================*/
{
public part1.test7(); // 01 默认构造方法
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lpart1/test7;
public static void main(java.lang.String[]); // 02 main方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
类的方法详细信息
javap -c test7.class
----------------------------------------------------------------------------------
Compiled from "test7.java"
public class part1.test7 {
public part1.test7();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
可以看到:方法中通过#数字在常量池中查找对应的路径。
常量池作用:常量池本质上是一个常量表,虚拟指令可以通过常量池查找到执行的类名,方法名,参数类型,字符串等信息。
2-6-5 运行时常量池的概念
运行时常量池的概念:可以理解为加载到内存的常量表,原本的常量池是被存储在class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址**
- 常量池的本质是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
2-7 StringTable(串池)的理解
2-7-1运行时常量池与串池的关系
public class test8 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
}
}
上述代码对应的虚拟机指令
public static void main(java.lang.String[])
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
}
执行过程
step1
class文件中的常量池的信息会被加载内存中,这个时候字符串对象"a","b","ab"在对应的使用命令
没有执行前都还只是常量池中的符号,还没有变为Java字符串对象
StringTable [] 此时为空,注意它是hash结构,不能自动扩容。
step2
(lazy load mechansim)
当执行ldc #2命令,根据#2找到串a并将其转换为字符串对象"a",放入串池
当执行ldc #3命令,根据#3找到串b并将其转换为字符串对象"b",放入串池
当执行ldc #4命令,根据#4找到串a并将其转换为字符串对象"ab",放入串池
StringTable ["a" "b" "ab"]
总结:JVM在执行指令的过程(懒加载)中从常量池中获取数据并加入串池。
- 注意串池中的串对象都是延迟加载,用到这个串的时候才会将其添加到串池中。串池本质上就是个hash表。
2-7-2 例题分析:字符串变量的拼接
问题:s3,s4这二个对象在虚拟机中对象地址是否相同?
public class test8 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1+s2;
System.out.println(s3==s4);
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return
LineNumberTable:
line 24: 0
line 25: 3
line 26: 6
line 27: 9
line 41: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
}
SourceFile: "test8.java"
根据上面的字节码分析:
public class test8 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
/*
s4的对象生成有以下几个步骤:
1)创建stringBuilder对象,使用无参构造函数。
2)从StringTable[1]和StringTable[2]分别获取2个对象"a"与“b”
3)执行StringBuilder.append("a").append("b")
4)调用toString方法得到新的String对象,其值也为“ab"
该对象位于堆中,由于toString中的new关键字
*/
String s4 = s1+s2;
System.out.println(s3==s4);
}
}
总结:可以看到对象s4的创建利用了串池的s1与s2,并生成了一个新的对象放入堆中,其串值与串池的s3相同。虽然串值相同,但是一个在堆中一个在串池中,因此属于不同的对象。所以是false.
2-7-3 例题分析:常量字符串的拼接
public class test8 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;
String s5 = "a"+"b";
}
}
变量字符串拼接
String s4 = s1+s2; // stringbulider.append(s1).append(s2).toString()生成新的字符串对象
常量字符串拼接
String s5 = "a"+"b"; // 由于都是字符串常量,属于不可变对象,编译时即可以确定”a"+"b"就是”ab"
// s5中会从串池中获取对象,不会有新的对象生成。
注意区分以上二种拼接。
2-7-2 String的intern方法
注意:JDK1.6与JDK1.8对该方法的处理不同
JDK1.8作用:尝试将堆中的字符串对象放入到串池中,返回串池中的对象。
- 串池中有则不会放入,没有则放入。
JDK1.6作用:尝试将堆中的字符串对象放入到串池中,返回串池中的对象
- 串池中有则不会放入,没有则将堆中的对象复制一份放入,注意此时堆中对象依旧存在
JDK1.8实例1:串池中没有该对象
public class test9 {
public static void main(String[] args) {
// 由于使用的时new关键字,下面一行代码创建的对象都是在堆中,不是串池中的字符串常量
String s = new String("a") + new String("b"); // 最终效果 new String("ab);
String s2 = s.intern();
// 将堆中字符串对象放入串池中,如果有则不会放入,如果没有则放入串池,并把串池中的对象返回。
System.out.println(s == s2); // true , 堆中的对象成功放入
System.out.println(s2 == "ab"); // true
}
}
JDK1.8实例2;串池中已经有该对象
package part1;
public class test9 {
public static void main(String[] args) {
String x = "ab";
// 由于使用的时new关键字,下面一行代码创建的对象都是在堆中,不是串池中的字符串常量
String s = new String("a") + new String("b"); // 最终效果 new String("ab);
String s2 = s.intern(); // 将堆中字符串对象放入串池中,如果有则不会放入,如果没有则放入串池,
System.out.println(x == s2); // true
System.out.println(s2 == "ab"); // true
System.out.println(s == s2); // false,可见堆中的对象没有成功放入
}
}
2-7-3 StringTable例题综合分析(基于JDK1.8)
package part1;
public class test8 {
public static void main(String[] args) {
String s1 = "a"; // StringTable = {"a"}
String s2 = "b"; // StringTable = {"a","b"}
String s3 = "ab"; // StringTable = {"a","b","ab"}
String s4 = s1+s2; // 生成字符串对象放入堆中,值为"ab"
String s5 = "a"+"b"; // StringTable = {"a","b","ab"}
String s6 = s4.intern(); // 尝试将s4对象放入串池,已经存在则不放了,返回串池中的对象给s6
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
System.out.println(s4 == s6); // false,堆中对象与串池中对象
// 创建对象string("cd")放入堆中
String x2 = new String("c") + new String("d");
// 问,如果调换了【下面两行代码】的位置呢
String x1 = "cd";
x2.intern();
System.out.println(x1 == x2); // true。调转顺序fasle
}
}
2-7-4 StringTable的位置
StringTable(串池)的位置与版本有关。
对于Java1.6来说
StringTable的是常量池的一部分,所在的空间称之为永久带。
对于Java1.8来说
StringTable的位置被迁移到堆中。
原因:在堆中StringTable的垃圾回收效率更高,StringTable在永久带触发垃圾回收的条件比较苛刻。
2-7-5 StringTable的垃圾回收机制
- 当内存空间不足的时候,会触发垃圾回收机制去释放StringTable中字符串对象。
JDK1.8验证代码
package part1;
/*
验证StringTable也参与垃圾回收。
虚拟机参数:
-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
-Xmx10m 设置堆内存最大为10M
-XX:+PrintStringTableStatistics 打印StringTable的统计信息
-XX:+PrintGCDetails -verbose:gc 打印垃圾回收的相关信息
*/
public class test10 {
public static void main(String[] args) {
int i = 0;
try{
/*四行代码是否删除测试*/
// for (int j = 0; j < 100000; j++) { // j=100, j=10000
// String.valueOf(j).intern();
// i++;
// }
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
执行结果(四行代码删除)
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->739K(9728K), 0.0076104 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
0
Heap
PSYoungGen total 2560K, used 569K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1a700,0x00000000fff00000)
from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 251K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 3% used [0x00000000ff600000,0x00000000ff63ed70,0x00000000ffd00000)
Metaspace used 3540K, capacity 4536K, committed 4864K, reserved 1056768K
class space used 395K, capacity 428K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 14292 = 343008 bytes, avg 24.000
Number of literals : 14292 = 607904 bytes, avg 42.535
Total footprint : = 1111000 bytes
Average bucket size : 0.714
Variance of bucket size : 0.719
Std. dev. of bucket size: 0.848
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 1781 = 42744 bytes, avg 24.000
Number of literals : 1781 = 158824 bytes, avg 89.177
Total footprint : = 681672 bytes
Average bucket size : 0.030
Variance of bucket size : 0.030
Std. dev. of bucket size: 0.172
Maximum bucket size : 2
观察StringTable statistics的信息
- buckets的大小是60013
- 已经使用的bucket是1781
没有删除四行代码的执行结果
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->739K(9728K), 0.0020074 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2787K->759K(9728K), 0.0012225 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2807K->759K(9728K), 0.0031246 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
100000
Heap
PSYoungGen total 2560K, used 1895K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 68% used [0x00000000ffd00000,0x00000000ffe5fca0,0x00000000fff00000)
from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 271K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 3% used [0x00000000ff600000,0x00000000ff643d80,0x00000000ffd00000)
Metaspace used 3543K, capacity 4536K, committed 4864K, reserved 1056768K
class space used 395K, capacity 428K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 14293 = 343032 bytes, avg 24.000
Number of literals : 14293 = 607920 bytes, avg 42.533
Total footprint : = 1111040 bytes
Average bucket size : 0.714
Variance of bucket size : 0.719
Std. dev. of bucket size: 0.848
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 26790 = 642960 bytes, avg 24.000
Number of literals : 26790 = 1559632 bytes, avg 58.217
Total footprint : = 2682696 bytes
Average bucket size : 0.446
Variance of bucket size : 0.426
Std. dev. of bucket size: 0.653
Maximum bucket size : 4
观察StringTable statistics的信息,由于限制了堆内存的大小,因此当将100000个字符对象放入StringTable造成堆内存不够使用,所以进行了垃圾回收。
- 可以看到当前堆内存下,最多开26790个bucket。
2-7-6 StringTable的性能调优以及应用场景
调优关键:主要通过JVM参数调整桶的大小(StringTable的大小)。
-XX:StringTableSize=桶个数 (最小值系统规定为1009)
原理:StringTable可以看作hashTable,hashTable的bucket比较小的话(小于不同字符串的数量),插入大量的字符串会造成单个bucket中存放大量元素。这样会导致hash表插入的效率下降,因为每次插入操作都需要花费时间去确定该元素是否已经存在。这种场景下可以通过增加bucket的大小来提高性能。
应用场景:项目中需要重复利用大量的字符串,如果将字符串对象仅仅只是放入到堆内存中的话,会出现堆中有大量的重复的字符串对象,造成内存的大量占用。
但是如果将这些字符串对象放入到常量值,由于常量池中,单一值的对象仅仅出现一次,因此能够极大的节约内存。
3 JVM相关的直接内存(并非虚拟机的内存管理,而是操作系统内存)
3-1 直接内存的特点
1)垃圾回收机制不会管理直接内存
- 实际中直接内存的分配与会回收是通过Java的底层unsafe对象(该对象只能通过反射获取,该对象利用本地方法分配内存)管理的。
/*下面是unsafe类中提供的一些内存操作的方法,可以看懂内存的方法都是本地方法*/
public native long allocateMemory(long var1);
public native long reallocateMemory(long var1, long var3);
public native void setMemory(Object var1, long var2, long var4, byte var6);
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);
public native void freeMemory(long var1);
2)直接内存通常用于NIO操作,作为数据缓冲区。
3)分配回收成本比较高(需要操作系统进行内存调度),此外使用直接内存相比较普通的IO读写性能高。
3-2 为什么直接内存的效率高
总结
采用普通IO:数据需要拷贝2次,磁盘数据拷贝到系统缓冲区(操作系统可以用,JVM无法用),然后再由系统缓冲区拷贝到JVM管理的缓冲区(操作系统不能用,JVM可以用)。
直接内存:划分出一块内存可以让Java直接访问。(操作系统与JVM都可以用)
3-3 直接内存溢出
测试代码
package part1;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class test11 {
static int _100Mb = 1024*1024*100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try{
while(true){
ByteBuffer bytebuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(bytebuffer);
i++;
}
}finally {
System.out.println(i);
}
}
}
执行结果
17
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at part1.test11.main(test11.java:14)
Process finished with exit code 1
3-4 直接内存的分配与回收原理(依赖于Unsafe对象)
1)直接内存的分配与回收依需要调用Java的Unsafe对象,通过Unsafe对象能够手动的去分配与回收。
实例
- windows平台通过任务管理管理器可以监测到该程序在执行过程使用了500MB内存
package part1;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
class UnsafeAccessor {
static Unsafe unsafe;
static {
Field theUnsafe = null;
try {
theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
static Unsafe getUnsafe() {
return unsafe;
}
}
public class test12 {
static int _500MB = 512*1024*1024;
public static void main(String[] args) throws InterruptedException {
Unsafe unsafe = UnsafeAccessor.getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_500MB);
unsafe.setMemory(base,_500MB,(byte)0);
System.out.printf("分配500MB内存!\n");
Thread.sleep(10000);
unsafe.freeMemory(base);
System.out.printf("释放500MB内存!\n");
}
}
2)ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
/*ByteBuffer.allocateDirect(_100Mb)的源码分析*/
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
- 可以看到内存的分配调用的是unsafe.allocateMemory方法
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
- 内存的释放采用的 unsafe.freeMemory(address)
- 注意这里内存释放的时机是ByteBuffer对象被垃圾回收。
3-5 显示的垃圾回收禁止对于直接内存释放带来的影响
问题:
- 通过下面的虚拟机参数能够让显式的垃圾回收指令System.gc()失效。这样可能会导致分配的直接内存不能使用完后立即释放,浪费操作系统的内存资源。
解决策略:
- 通过Unsafe对象在使用完之后手动的去释放内存资源。
-XX:+DisableExplicitGC
package cn.itcast.jvm.t1.direct;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* 禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
/*
* -XX:+DisableExplicitGC 显式的
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
参考资料
20210403