Java内存结构
1. Program Counter Register 程序计数器
- 作用:记住下一条jvm指令的执行地址
比如说执行指令0的时候,程序计数器会记住3,当0执行结束后,解释器会去程序计数器里取3的指令,依次类推。
指令经过解释器,解释成机器码,机器码交给CPU执行 - 特点:
- 线程私有:每个线程都有自己的程序计数器
例子:两个线程运行,cpu会有一个调度器组件,给他们分配时间片,cpu给线程1分配一个时间片,在时间片内,1的代码没有执行完,会将线程1的状态进行一个暂存,切换到线程2,当线程2的代码执行到一定程度,线程2的时间片用完了,再切换回来继续执行线程1剩余代码(比如执行到9的时候切换到线程2了,线程2执行完了,切换到线程1,因为程序计数器记录的是下一条,所以从10继续执行)。
- 线程私有:每个线程都有自己的程序计数器
- 不会存在内存溢出
2. Java Virtual Machine Stacks虚拟机栈
-
定义:每个线程运行时所需要的内存,称为虚拟机栈
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
方法1,2,3每次调用对应一个内存空间的占用 -
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
-
问题辨析:
1.垃圾回收是否涉及栈内存?
不涉及,因为栈内存无非是一次次方法调用产生的栈帧内存。而栈帧内存在每一次方法调用结束后,会弹出栈,即自动的被回收,所以不需要垃圾回收来管理栈内存。
2.栈内存分配越大越好吗?
过大的栈内存,会导致可运行线程数减少,越大只是能进行更多次的递归方法调用
3.方法内的局部变量是否线程安全?
方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经称为解决并发问题的一种途径。同时还有个响亮的名字叫做 线程封闭。
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。 -
栈内存溢出
1.栈帧过多导致栈内存溢出(方法不断调用,比如递归 )
2.栈帧过大导致栈内存溢出 -
线程运行诊断:
定位: 用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
case1:cpu占用过多:
case2:程序运行很长时间没有结果
3.本地方法栈
- 作用:java虚拟机在调用一些本地方法时需要给这些本地方法(c/c++编写的代码)提供内存空间,比如Object类的notify方法
4. 堆
- 通过new关键字创建对象都会使用堆内存
- 特点:
- 线程共享,堆中的对象都要考虑线程安全问题
- 有垃圾回收机制 - 堆内存溢出
-Xmx8m
public class Demo1_5 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<String>();
String a = "hello";
while(true){
list.add(a); //hello
a = a + a; //hello,hellohello,hellohellohellohello.......
i++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
}
java.lang.OutOfMemoryError
at java.lang.AbstractStringBuilder.hugeCapacity(AbstractStringBuilder.java:161)
at java.lang.AbstractStringBuilder.newCapacity(AbstractStringBuilder.java:155)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:125)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at cn.itcast.jvm.Demo1_5.main(Demo1_5.java:14)
-
tips:
-程序计数器、虚拟机栈、本地方法栈都是线程私有的
-堆和方法区都是线程公有的 -
堆内存诊断
1.jps 工具
查看当前系统中有哪些 java 进程
2.jmap 工具
查看堆内存占用情况 jmap - heap 进程id
3.jconsole 工具
终端输入:
jps
jmap -heap 进程id
jconsole
jvisualvm
图形界面的,多功能的监测工具,可以连续监测
5.方法区
- 定义:
《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
所以,方法区看作是一块独立于Java堆的内存空间。
方法区主要存放的是 Class,而堆中主要存放的是实例化的对象 - 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
- 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:ava.lang.OutofMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace
- 加载大量的第三方的jar包
- Tomcat部署的工程过多(30~50个)
- 大量动态的生成反射类
- 关闭JVM就会释放这个区域的内存。
在jdk7及以前,习惯上把方法区,称为永久代。jdk8开始, 使用元空间取代了永久代。本质上,方法区和永久代并不等价。仅是对hotspot而言的。 - 内部结构
-
方法区内存溢出:
元空间可能产生内存溢出,动态代理在运行期间动态生成类的字节码,来完成动态的类加载,比如spring、mybatis在运行期间产生大量的类,可能导致元空间内存溢出 -
常量池与运行时常量池:
-
方法区内部包含运行时常量池。字节码文件内部包含常量池。
-
java经过编译后生成的.class文件,是Class文件的资源仓库,一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table) ,包括各种字面量和对类型、域和方法的符号引用。
-
常量池:可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
数量值
字符串值
类引用
字段引用
方法引用 -
运行时常量池:运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
例如:#1会变为真实地址 -
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
-
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。而运行时常量池期间也有可能加入新的常量(如:String.intern方法)当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常
串池(StringTable): -
常量池中的字符串仅是符号,第一次用到时才变为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是 StringBuilder (1.8)
-
字符串常量拼接的原理是编译期优化
-
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
-
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串
-
池中的对象返回
-
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,
-
放入串池, 会把串池中的对象返回
字符串拼接操作 -
常量与常量的拼接结果在常量池,原理是编译期优化
-
常量池中不会存在相同内容的变量
-
只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
-
如果拼接的结果调用intern()方法(intent方法会将对象放入串池)则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
intern()的使用 -
intern是一个native方法,调用的是底层C的方法
-
字符串池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。
-
如果不是用双引号声明的string对象,可以使用string提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
比如:
String myInfo = new string("I love atguigu").intern();
-
也就是说,如果在任意字符串上调用string.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
(“a”+“b”+“c”).intern()==“abc” -
通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省内存空间。
案例分析:
new String("a") + new String("b") 会创建几个对象
/**
* new String("ab") 会创建几个对象? 看字节码就知道是2个对象
*
* @author: mason
* @create: 2022-01-27-11:17
*/
public class StringNewTest {
public static void main(String[] args) {
String str = new String("a") + new String("b");
}
}
字节码文件为
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return
我们创建了6个对象
对象1:new StringBuilder()
对象2:new String("a")
对象3:常量池的 a
对象4:new String("b")
对象5:常量池的 b
对象6:toString中会创建一个 new String("ab")调用toString方法,不会在常量池中生成ab
public class Demo1_23 {
// ["a", "b","ab"]
public static void main(String[] args) {
//String x = "ab";
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
// 这里是将ab放入串池
String s2 = s.intern();
System.out.println( s2 == "ab"); true,因为intern返回的是放入串池中的对象
System.out.println( s == "ab");true,因为将s这个字符串放入串池
// System.out.println( s == x );
}
}
public class Demo1_23 {
// ["ab", "a", "b"]
public static void main(String[] args) {
String x = "ab";//放入串池
/**
创建StringBuffer,放入串池,创建a对象,放入串池,创建b对象,StringBUffer.toString+
新对线ab在堆
*/
String s = new String("a") + new String("b");
// 堆 new String("a") new String("b") new String("ab")
// 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
// 这里是将ab放入串池
String s2 = s.intern();
System.out.println( s2 == "ab"); true,因为intern返回的是放入串池中的对象
System.out.println( s == "ab");true,因为将s这个字符串放入串池
// System.out.println( s2 == x );true。s2是串池中的对象
//System.out.println(s == x) false ,s 在堆里。x在串池里
}
}
参考:蘑菇博客 http://www.moguit.cn