(1)一次编译,到处运行
(2)自动内存管理,垃圾回收功能
(3)数组下标越界检查
(4)多态
(1)记住下一条JVM指令的执行地址
(2)使用寄存器做程序计数器
(3)线程私有的
(4)不会存在内存溢出
(1)每个线程运行时所需要的内存
(2)每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
(3)每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
(1)垃圾回收不涉及栈内存,因为每一个方法对应一个栈帧,每个方法运行结束后,栈帧就会自动出栈
(2)栈内存不是分配越大越好,因为栈内存分配越大,线程数量就会越少
(3)方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果局部变量引用了对象,并逃离了方法的作用范围,它就存在线程安全的风险
(1)栈帧过多导致栈内存溢出(方法递归调用时)
(2)栈帧过大
(1)某个程序CPU占用过多
第一步:用 top 定位哪个进程对CPU的占用过高
第二步:用 ps H -eo pid,tid,%cpu | grep 进程id 来定位这个进程中哪个线程对CPU占用过高
第三步:用 jstack 进程id ,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
(2)程序运行很长时间没有结果
第一步:jstack 进程id :划到最后可以看出可能是线程死锁
给本地方法运行提供内存空间
(1)通过new关键字创建的对象都会使用堆内存
(2)是线程共享的,堆中对象都需要考虑线程安全的问题
(3)有垃圾回收机制
public class TestOOM {
public static void main(String[] args) {
int i=0;
try {
List<String> list=new ArrayList<>();
String a="hi";
while (true){
list.add(a);
a=a+a;
i++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
}
(1)jps工具
查看当前系统中有哪些java进程
(2)jmap工具
jmap -heap 进程id :查看堆内存占用情况
(3)jconsole工具
图形界面的,多功能的检测工具,可以连续监测
(1)jps:查看进程id
(2)jmap -heap 进程id :查看堆内存占用
(1)线程共享的
(2)存储了和类的结构相关的数据,类的成员变量、方法数据、成员方法以及构造器方法的代码
(3)在JVM启动时被创建
(4)JDK1.7时方法区(永久代)在堆内存,JDK1.8(元空间)在直接内存
(1)JDK1.8之后,方法区在直接内存,没有设置上限,不容易出现方法区内存溢出
(1)常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等
(2)运行时常量池:常量池是class文件中的,当该类被加载时,它的常量池信息就会放入
运行时常量池,并把里面的符号地址变为真实地址
(1)常量池中的字符串仅是符号,第一次用到时才变为对象
(2)利用字符串常量池的机制,来避免重复创建字符串对象
(3)字符串变量拼接的原理是StringBuilder
(4)字符串常量拼接的原理是编译器优化
(5)可以使用 intern 方法,主动将字符串常量池中还没有的字符串对象放入串池
将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
public class TestStringTable {
public static void main(String[] args) {
//StringTable ["a" "b" "ab"]
/**
* 常量池中的信息都会被加载到运行时常量池中
* 这时 a b ab 都是常量池中的符号,还没有变成字符串对象
* ldc #2 会把 a 符号变为 "a" 字符串对象
* ldc #3 会把 b 符号变为 "b" 字符串对象
* ldc #4 会把 ab 符号变为 "ab" 字符串对象
*/
String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;//new StringBuilder().append("a").append("b").toString() 堆中:new String("ab")
String s5="a"+"b";//javac在编译期间做了优化 结果已经在编译器确定为ab
System.out.println(s3==s4);//false
System.out.println(s3==s5);//true
}
}
public class TestStringTable {
public static void main(String[] args) {
String s=new String("a")+new String("b");
/**
* 此时:
* 字符串常量池中:["a","b"]
* 堆中:new String("a") new String("b") new String("ab")
*/
//将s这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入,会把串池中的对象返回
String s2=s.intern();
//此时字符串常量池:["a","b","ab"]
System.out.println(s2==s);//true
System.out.println(s2=="ab");//true
System.out.println(s=="ab");//true
}
}
public class TestStringTable {
public static void main(String[] args) {
String x="ab";
String s=new String("a")+new String("b");
/**
* 此时:
* 字符串常量池中:["a","b","ab"]
* 堆中:new String("a") new String("b") new String("ab")
*/
//将s这个字符串对象尝试放入串池,如果串池中有则不会放入,如果没有则放入,会把串池中的对象返回
String s2=s.intern();
System.out.println(s2==x);//true
System.out.println(s==x);//false
System.out.println(s=="ab");//false
}
}
垃圾回收
(1)当空间不足时,将没有用的字符串常量垃圾回收
性能调优
(1)调整StringTable的大小
(2)将StringTable的size调大,可以减少哈希冲突,速度会变快
(3)可以使用 inter() 来避免重复的字符串对象入池,从而减少需要的内存
(1)常见于NIO操作时,用于数据缓冲区
(2)分配回收成本较高,但读写性能高
(3)不受JVM内存回收管理
(1)使用 Unsafe 类分配和释放内存,并且回收时需要主动调用 freeMemory()
(2)ByteBuffer的实现类内部使用了 Cleaner(虚引用) 来监测ByteBuffer对象,一旦
ByteBuffer对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的clean
方法调用freeMemory方法来释放内存
(1)为了不影响程序执行效率,可能会禁用 System.gc(),这样的话就不会自动进行直接内存回收
(2)需要我们手动调用 unsafe.freeMemory() 来释放直接内存
(1)某个对象被其它对象引用,则计数+1;如果该引用释放,则计数-1,当计数为0时,就可被回收
(2)但存在循环引用,也就是两个对象互相引用的情况
(1)确定根对象,扫描堆内存中所有的对象,如果某个对象被根对象直接或间接引用,则标记它,
表示该对象不能被回收;没有被标记的对象则可以被垃圾回收
(2)哪些对象可以作为根对象:java虚拟机栈中的引用的对象,方法区中的类静态属性引用对象
方法区中的常量引用对象,本地方法栈中的引用对象
(1)平常通过 = 创建的对象
(2)只有当没有引用指向该对象时,该对象才可以被垃圾回收
(1)当发生垃圾回收并且内存不足时,一次垃圾回收后内存依然不足,只有软引用引用的对象会被回收
(2)一些不重要的数据可以用软引用,在内存不足时可以进行回收
(3)如果使用了引用队列,当软引用引用对象被回收时,该软引用会放入引用队列,
是为了回收软引用
public class Test {
private static final int _4MB=4*1024*1024;
public static void main(String[] args) {
soft();
}
//软引用
private static void soft(){
//list --强引用--> SoftReference --弱引用--> byte[]
List<SoftReference<byte[]>> list=new ArrayList<>();
for (int i=0;i<5;i++){
SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB]);
list.add(ref);
System.out.println(list.size());
}
System.out.println(list.size());//1
}
//带队列的软引用
private static void soft1(){
//list --强引用--> SoftReference --弱引用--> byte[]
List<SoftReference<byte[]>> list=new ArrayList<>();
//引用队列
ReferenceQueue<byte[]> queue=new ReferenceQueue<>();
for (int i=0;i<5;i++){
//关联了引用队列,当软引用所指向的byte[]被回收时,软引用自己会加入到queue中
SoftReference<byte[]> ref=new SoftReference<>(new byte[_4MB],queue);
list.add(ref);
System.out.println(list.size());
}
System.out.println(list.size());//1
//从队列中获取无用的软引用对象,并移除
Reference<?extends byte[]> poll= queue.poll();
while (poll!=null){
list.remove(poll);
poll= queue.poll();
}
}
}
(1)只要发生垃圾回收,不管内存充不充足,只有弱引用引用的对象都会被回收
(2)如果使用了引用队列,当弱引用引用对象被回收时,该弱引用会放入引用队列,
是为了回收弱引用
(1)虚引用必须配合引用队列使用
(1)将没有被根对象引用的对象标记
(2)将被标记的对象释放(将垃圾对象的起始和结束地址进行记录)
(3)比较快,但容易产生内存碎片
(1)将没有被根对象引用的对象标记
(2)在清除垃圾的过程中,将可用的对象移向一端
(3)不会造成内存碎片问题,效率低
(1)将内存区域划分为大小相等的区域
(2)将没有被根对象引用的对象标记
(3)将from区域存活的对象复制到to区域
(4)清除from中的垃圾,交换from和to的位置
(5)不会存在内存碎片,需要双倍内存空间
(1)对象首先分配在伊甸园区域
(2)新生代空间不足时,触发 Minor GC,伊甸园和from存活的对象使用复制算法复制到to中,
存活的对象年龄+1,释放垃圾,交换from和to
Minor GC 会引发 stop the world,暂停其它用户线程,等垃圾回收结束,用户线程恢复运行
(3)当对象寿命超过阈值(最大15次)时,会晋升到老年代
(4)当老年代空间不足时,会先进行 Minor GC,当 Minor GC 后空间依然不足,会触发 Full GC
Full GC 也会引发 stop the world,比 Minor GC 引发的时间更长
(5)此时空间仍然不足时,会触发OOM
(1)单线程
(2)适合堆内存较小,适合个人电脑
(3)-XX:+UseSerialGC = Serial + SerialOld 打开串行垃圾回收器
(1)多线程
(2)适合堆内存较大,必须多核CPU
(3)垃圾回收时,让单位时间内 stop the world 的时间最短
(1)多线程
(2)适合堆内存较大,必须多核CPU
(3)垃圾回收时,尽可能的让 stop the world 的单次时间最短
(4)内存碎片可能过多,导致并发失败,
(1)注重吞吐量和低延迟
(2)适合超大堆内存,会将堆划分为多个大小相等的Region
(3)整体上是标记整理算法,两个区域之间用的是复制算法
回收伊甸园区的垃圾对象
(1)在 Young GC 时会进行 GC Root 的初始标记
(2)老年代占用堆内存空间比例达到阈值时,进行并发标记
(1)最终标记,将可能没有被标记的垃圾对象标记
(2)对伊甸园区、幸存区、old区进行回收
(3)伊甸园区的存活的对象会被复制到幸存区,幸存区达到条件的存活对象会晋升到老年代,其余
也复制到另一个幸存区。垃圾对象将被回收
(4)老年代的垃圾对象会根据设置的暂停时间被有选择的回收,如果某个区域垃圾对象被回收的时间会
超过暂停时间,那么就不会回收;如果不会超过暂停时间的,则会回收
(1)当垃圾回收的速度跟不上垃圾产生的速度时,并发收集失败,开始 Full GC
将引用新生代对象的老年代对象标记(将卡表中的卡标记为脏卡)
(1)根据对象的黑白状态,来确定是否被回收
(2)可能存在被强引用的对象被标记为白色,而被垃圾回收
(3)采用写屏障技术,在某个对象的引用改变前,会将该对象加入队列,在remark阶段会重新扫描队列
字符串在底层是由char[]数组表示
(1)将所有新分配的字符串放入一个队列
(2)在新生代回收时,G1并发检查是否有字符串重复
(3)如果它们值一样,让他们引用同一个 char[]
(4)优点:节省大量内存 缺点:略微多占用了CPU时间,新生代回收时间略微增加
(1)所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,
则卸载它们所加载的所有类
(1)一个对象大于region的一半时,称之为巨型对象
(2)G1不会对巨型对象进行拷贝
(3)回收时被优先考虑
(4)G1会追踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代
垃圾回收时被处理掉
并发标记必须在堆空间占满前完成,否则退化为 Full GC
0~3字节,表示它是否是class类型的文件
4~7字节,表示类的版本
记录java类中的各种信息
8~9字节,表示常量池长度,00 23 23的十进制就是35 表示常量池有#1~#34项,#0不计入,也没有值
第#1项0a表示一个method信息,00 06 和 00 15 表示它引用了常量池中#6和#21项来获得这个方法的
所属类和方法名
字节分析类文件结构太麻烦了,可以使用javap工具来反编译class文件
javap -v xxx.class
-v:显示详细信息
验证
验证类是否符合JVM规范,安全性检查
准备
(1)为静态变量分配空间,设置默认值,需要在初始化阶段完成赋值
(2)静态变量存储在堆中
(3)如果静态变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
(4)如果静态变量是final的,但数于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用
(1)即调用 ()
会导致初始化的情况:
(2)main方法所在的类,总会被首先初始化
(3)首次访问这个类的静态变量或静态方法时会导致初始化
(4)子类初始化时,如果父类还没初始化,那么会引发父类初始化
(5)子类访问父类的静态变量,只会触发父类的初始化
(6)调用Class.forName会触发初始化
(7)new 会导致初始化
不会导致类初始化的情况:
(1)访问类的 static final 静态常量(基本类型和字符串) 不会触发初始化
(2)类对象.class不会触发初始化
(3)创建该类的数组不会触发初始化
(4)类加载器的 loadClass 方法,不会导致初始化
(5)Class.forName 的参数2为false时不会导致初始化
(1)加载 JAVA_HOME/jre/lib
(1)加载 JAVA_HOME/jre/lib/ext
(1)加载 classpath
(1)加载 自定义
什么时候需要自定义类加载器:
(2)想加载非 classpath 随意路径中的类文件
(3)都是通过接口来使用实现,希望解耦时,常用在框架设计
(4)这些类希望予以隔离,不同应用的同类名都可以加载,不冲突,常见于tomcat
步骤:
(1)继承 ClassLoader 父类
(2)要遵从双亲委派机制,重写 findClass 方法
(3)读取类文件的字节码
(4)调用父类的 defineClass 方法来加载类
(5)使用者调用该类加载器的 loadClass 方法
(1)沙箱安全机制,防止核心API被篡改
(2)代码复用,如果一个类已经被加载,那么不会被重复加载
(3)稳定性,可以确保java类库的稳定性