编译的时候生成字节码文件,而运行的时候执行字节码文件
1、虚拟机栈
局部变量表和操作数栈的深度在编译期就可以确定下来,并且在后续过程中不会发生改变
2、栈
- 一个Java进程对应一个JVM实例,而一个JVM实例只有一个运行时数据区;当运行多个Java程序时,开启的是多个进程,也就是同时开启了多个JVM
- 调整栈的大小并不能保证完全避免StackOverflowError,而只能延迟该错误的出现时间
虚拟机栈并没有垃圾回收机制,随着出栈就会自动将引用销毁
- 当变量在方法内部产生、消亡时,可以说这个方法是线程安全的
3、堆
- 设置堆空间大小的参数
- -Xms 用来设置堆空间(新生代和老年代)的初始内存大小
- -Xmx 用来设置堆空间(新生代和老年代)的最大内存大小
其中 -X 是JVM的运行参数, ms 是 memory start
- 可以在Edit Configuration中配置VM options对JVM运行参数进行相关配置
【建议将Xms和Xmx设置成一个值,防止频繁扩容和释放操作】
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
}
- 关于设置的是
600M
而显示的是575M
的原因探究及查看JVM设置参数是否生效的两种方法
- 方法一
运行如下main方法:【注意一定要在运行期间在cmd中查看,所以使用sleep方法使线程睡眠】
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通过下面的方法可以详细的看到堆内存的分配情况
Java将堆内存在逻辑上分为新生代和老年代
而新生代又分为伊甸园区、幸存者-0、幸存者-1区
而600 = S0C+S1C+EC+OC
,575 =S0C/S1C+EC+OC
而查看到的是575M的原因为:由于垃圾回收的复制算法,两个幸存者区必须有一个始终为空,而能够存放对象的只有伊甸园区和一个幸存者区
- 方法二
在 VM Options中添加-XX:+PrintGCDetails
【不需要保持查看期间进程运行,相反,这种查看JVM参数的方法需要的时间点在进程结束后】
- 有关TLAB
下面的内容原文链接点击此处
TLAB空间一般是不会太大,则较大对象肯定无法在此分配,正是TLAB空间的局限性,对象可能很容易将此空间装满。
如一个线程,他有100k的TLAB空间,现在已存入对象占用了80k,现在有个30k的对象肯定无法满足对空间的需求,
所以它存在两种解决方式:
第一种就是放弃当前的TLAB空间,把他还给堆空间,创建新的TLAB。
第二种就是让这个30k的对象在堆空间分配,保留TLAB空间,或许后面会有小于等于20k的对象。
为了解决以上的情况,虚拟机维护了一个refill-waste的值,表示TLAB允许内存浪费的空间。
默认是tlab空间的1/64(在运行时可能会自动调整达到最优化)。
当新的对象请求时空间内存已不足时,会让当前对象所需占用的空间内存和refill-waste比较,
若大于r-w会在堆中分配,保留tlab;若小于r-w会废弃当前Tlab空间新建一个分配对象
- 对象的大致分配流程:
- 第一:尝试栈上分配,如不成功第二步。
- 第二:尝试tlab分配,如不成功第三步。
- 第三:是否满足老年代分配,如不成功第四步。
- 第四:Eden分配。
4、方法区
- 有关堆、栈、方法区之间的交互关系
- 修改JVM运行环境(JDK),通过修改以下两处即可
- JDK8即前后方法区参数设置对比及注意事项
- 字节码文件是一个二进制文件,而要看具体的字节码指令时,需要对其进行反编译/解析(48-局部变量表的认识5:50 有说到反编译是什么)
而反编译的两种途径为:- 通过IDEA中的jclasslib插件,下载完成后可以通过下面的方法进行查看
- - 通过在IDEA中的Terminal中输入
javap -v/verbose -p 类.class
-p是为了看到访问权限较小的属性和方法,如被private修饰的
还可以通过javap -v/verbose -p 类.class > text.txt
获取到反编译后得到的txt文件,可以直接在编辑工具中打开便于查看-
- 通过IDEA中的jclasslib插件,下载完成后可以通过下面的方法进行查看
- 方法区中是可以看到类加载器具体信息的,而在编译后的字节码文件中是找不到的,因为还没有被加载器加载,在编译的时候,即上面看到的字节码指令,只是编译器生成的静态指令,还没有被类加载器执行
同样的道理,通过编译获得的字节码文件中包含了常量池,而运行字节码文件时,这个常量池就变成了运行时常量池
其中,String.intern()
可以动态的将对象添加到运行时常量池中
6.关于字符串常量池和静态变量的存放位置
- JDK6中,方法区和堆一样,使用JVM分配的内存,HotSpot虚拟机中,方法区的具体实现是永久代,并将静态变量和字符串常量池放在了永久代中
- JDK7中,逐渐实现“去永久代化”,将和静态变量和字符串常量池放进堆中
- JDK8中,摒弃了永久代,转而使用元空间作为方法区的实现模型,且元空间并不占用JVM的分配内存,而是直接使用物理内存,可以看到,静态变量和字符串常量池仍然保存在堆中
- 为什么要把永久代替换为元空间