JVM简记
仅做学习内容的简单记录
- java中基本类型:
byte 1个字节、chart 2个字节、short 2个字节、int 4个字节、float 4个字节、long 8个字节、double 8个字节、boolean(要看jvm把它按什么类型处理,boolean[]会当做byte[]类型处理就是1个字节,普通情况下当做int处理就是4字节,因为4字节为32位,32位CPU处理起来效率更高)
1.jdk命令
1.1 jps
jps -l 查看java进程
1.2 jstat
8591是进程的pid,250代表250ms采样间隔,4代表采样数
S0C:年轻代中第一个survivor(幸存区)的容量(字节)
S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
S0U :年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
S1U :年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
EC :年轻代中Eden(伊甸园)的容量 (字节)
EU :年轻代中Eden(伊甸园)目前已使用空间 (字节)
OC :Old代的容量 (字节)
OU :Old代目前已使用空间 (字节)
MC:metaspace(元空间)的容量 (字节)
MU:metaspace(元空间)目前已使用空间 (字节)
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC :从应用程序启动到采样时年轻代中gc次数
YGCT :从应用程序启动到采样时年轻代中gc所用时间(s)
FGC :从应用程序启动到采样时old代(全gc)gc次数
FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)
GCT:从应用程序启动到采样时gc用的总时间(s)
1.3 jinfo
jinfo -flags pid
可以看到jvm版本号,jvm的默认参数(Non-default)以及启动时设置的参数(command line)
1.4 jmap & jhat
histo命令: jmap -histo:live pid 描述:显示堆中对象的统计信息
使用 | head -10 就是通过管道符过滤只显示结果的前10行
C:char B:byte I:int
dump:命令:jmap -dump:format=b,file=heapdump.phrof pid 描述:生成堆转储快照dump文件。
2.jvm运行参数
GC日志相关参数
参数 | 含义 |
---|---|
-XX:+PrintGC | 输出GC日志 |
-XX:+PrintGCDetails | 输出GC的详细日志 |
-XX:+PrintGCTimeStamps | 输出GC的时间戳(以基准时间的形式) |
-XX:+PrintGCDateStamps | 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) |
-XX:+PrintHeapAtGC | 在进行GC的前后打印出堆的信息 |
-XX:+PrintGCApplicationStoppedTime | 输出GC造成应用暂停的时间 |
-Xloggc:…/logs/gc.log | 日志文件的输出路径 |
启动命令中堆栈空间参数
参数 | 含义 |
---|---|
-Xmx2048m | 等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。 |
-Xms512m | 等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。 |
-XX:PermSize(jdk1.8后废弃) | 设置永久代(perm gen)初始值。默认值为物理内存的1/64。 |
-XX:MaxPermSize(jdk1.8后废弃) | 设置持久代最大值。物理内存的1/4。 |
-XX:MetaspaceSize | 初始元空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 |
-XX:MaxMetaspaceSize | 最大元空间,默认是没有限制的。 |
jdk8后perm gen由metaspace代替 ,方法区移至元数据区,元空间替代了持久代。
–只记录相对重要的
3.class文件
3.1 常量池(cp_info)的结构是什么
3.2 int和float数据类型的常量在常量池中怎样存储
tag标识类型,后面接这个类型所占字节。
3.3 long和double数据类型的常量在常量池中怎样存储
这里可以看到是将这种8字节占用的类型分成了高4字节和低4字节
3.4 String数据类型的常量在常量池中怎样存储
tag标识类型,length标识长度,后面是字节长度,字符串长度是变的,所以用字节数组存
3.5 哪些字面量会进入常量池中?
- 【final修饰】的8种基本类型的值会进入常量池。
- 【非final类型】(包括static的)的8种基本类型的值,只有【double、float、long】的值会进入常量池。
- 常量池中包含的字符串类型字面量(【双引号引起来的字符串值】)。
4.类加载
4.1 类加载过程
4.2 类加载器
- 启动类加载器(Bootstrap ClassLoader)
- 负责加载 JAVA_HOMT\lib目录中的
- 或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类
- 由C++实现,不是ClassLoader子类
- 扩展类加载器(Extension ClassLoader)
- 负责加载 JAVA_HOME\lib\ext目录中的
- 或通过java.ext.dirs系统变量指定路径中的类库
- 应用程序类加载器(Application ClassLoader)
- 负责加载用户路径(classpath)上的类库
4.3 双亲委派
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrapClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
- 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,
- 只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
采用双亲委派的一个好处是:为什么要使用双亲委托这种模型呢?
- 比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
如何判定两个class是相同?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。
为什么要使用双亲委托这种模型呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
为什么要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时。
比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader类的全限定名完全相同,但是加载它的类加载器不同,那么在方法区中会产生不同的【Class】对象。
5.运行时数据区
5.1 Hotspot运行时数据区
jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generation 。
jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space 。
方法区是Java虚拟机规范中的定义,是⼀种规范,而永久代和元空间是 HotSpot 虚拟机不同版本的两种实现。
6.方法区
方法区是抽象出来的概念。而永久代和元空间是方法区的具体实现。
永久代和元空间的区别是什么?
1.JDK1.8之前使用的方法区实现是永久代、JDK1.8及以后使用的方法区实现是元空间。
2.永久代所使⽤用的内存区域是JVM进程所使用的区域,它的大小受整个JVM的大小所限制。元空间所使用的内存区域是物理内存区域。那么元空间的使用大小只会受物理内存大小的限制。
3.永久代存储的信息基本上就是上面方法区存储内容中的数据。元空间只存储类的元信息,而静态变量和运行时常量池都挪到堆中。
为什么要使⽤元空间来替换永久代?
1.Oracle收购了Java,而收购之前的Sun使用的JVM是Hotspot,方法区的实现是永久代,而Oracle自身也有一个JVM的实现,是JRockit,它的方法区就是元空间。收购之后,打算合⼆为⼀。
2.运行时常量池如果在JVM的内存中的话,如果使用不当,很容易出现内存溢出,因为永久代分配的大小,比较小。
永久代向元空间转换的原因:
- 字符串存在永久代中,容易出现性能问题和永久代内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 计划将HotSpot 与 JRockit 合二为一。
7.字符串常量池
7.1 三种常量池的区别
class常量池(静态常量池)、运行时常量池、字符串常量池区别:
- class常量池中存储的是符号引用,而运行时常量池存储的是被解析之后的直接引用。
- class常量池存在于class文件中,运行时常量池和字符串常量池是存在于JVM内存中。
- 运行时常量池具有动态性,java运行期间也可能将新的常量放入池中(String#intern()准确的说用这个方法会使其放入字符串常量池)
- 字符串常量池逻辑上属于运行时常量池的⼀部分,但是它和运行时常量池的区别在于,字符串池是全局唯一的,而运行时常量池是每个类一个。
7.2 存储位置
在JDK1.6及以前,运行时常量池是方法区的⼀部分。
在JDK1.7及以后,运行时常量池在Java 堆(Heap)中。
运行时和class常量池⼀样,运行时常量池也是每个类都有一个。但是字符串常量池全只有一个
7.3 字符串常量池存储数据
- 有专门的内存区域去存储字符串常量池中的字符串对象。
- 提供一个stringtable(hashtable)数据结构,用来提高匹配速度。
实际上,为了提高匹配速度,即更快的查找某个字符串是否存在于常量池,Java在设计字符串常量池的时候,是了一张stringtable, stringtable 有点类似于我们的hashtable,里面保存了字符串的引用。
在jdk6中 StringTable 的长度是固定的,就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长。此时当调用 String.intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降;
在jdk7+, StringTable 的长度可以通过一个参数指定:
-XX:StringTableSize=99991
7.4 部分等值测试
public static void testS() {
String str1 = "abc";//这种引号方式创建的都是直接在字符串常量池中创建,地址就是字符串常量池中对应的引用地址
String str2 = new String("abc");
System.out.println(str1 == str2);
String str3 = new String("abc");//new会创建两个对象,一个在运行时常量池一个在字符串常量池,new的话地址都是运行时常量池的地址
System.out.println(str3 == str2);
String str4 = "a" + "b"; // 直接在字符串常量池中创建"ab",如果没有的话
System.out.println(str4 == "ab");
final String s = "a";//字符串常量池中
String str5 = s + "b";
System.out.println(str5 == "ab");
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;//相当于new String("a"+"b")
System.out.println(str6 == "ab");
String str7 = "abc".substring(0,2);
System.out.println(str7 == "ab");
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");
String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern());//intern把他的引用 放到字符串常量池里
}
---运行结果
false
false
true
true
false
false
false
true
// String f = new String("boo") + new String("lean"); //f
// String f = new String("by") + new String("te"); //f
// String f = new String("ch") + new String("ar"); //f
// String f = new String("de") + new String("fault"); //f
// String f = new String("dou") + new String("ble"); //f
// String f = new String("fal") + new String("se"); //f
// String f = new String("flo") + new String("at"); //f
// String f = new String("in") + new String("t"); //f
// String f = new String("l") + new String("ong"); //f
// String f = new String("nu") + new String("ll"); //f
// String f = new String("sh") + new String("ort"); //f
// String f = new String("tr") + new String("ue"); //f
// String f = new String("vo") + new String("id"); //f
//String f = new String("ja") + new String("va"); //f
String f = new String("ja") + new String("va1"); //t
System.out.println(f.intern() == f);
String f2 = "java1";
String f = new String("ja") + new String("va1");
System.out.println(f.intern() == f);//f
System.out.println(f.intern() == f2);//t
查询String的intern源码,查看官方注释
根据官方注释说明如果intern时字符串常量池中有对应常量则直接返回字符串常量池中对应的引用,否则就将对象引用放到字符串常量池中再返回引用。
仅做学习记录与交流,如有错漏敬请指出