程序计数器记录下一条jvm指令的地址,然后由解释器将jvm指令翻译成机器码,交给cpu执行
程序计数器是线程私有的
程序计数器不会出现内存溢出
每个线程运行时所需要的内存,成为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存?
不会,栈内存无非就是一次次方法调用产生的栈帧内存,随着方法的执行完毕,内存会被释放。
栈内存分配越大越好吗?
栈内存分配的越大,同时运行的线程数量就越小。
方法内的局部变量是否线程安全?
如果变量是方法内的局部变量(对多个线程是私有的),则不会出现线程安全,因为每个方法执行时都会由自己的栈帧空间。
如果变量是方法外的共有变量(对多个线程是共享的),会出现线程安全问题
如果没有逃离方法的作用范围,就是线程安全的。(若作为返回值返回,则逃离了方法作用范围)
如果局部变量引用了对象,并且逃离了方法作用范围,则不是线程安全的。
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("A");
s1.append("B");
...
return s1;
}
// 此时非线程安全,但是如果返回的不是引用类型,则是线程安全的。
栈帧过多时会导致栈内存溢出。如:方法递归调用没有出口。
栈帧过大导致栈内存溢出。
cpu占用太高
定位:
用top命令定位哪个进程对cpu占用过高。
ps H -eo pid,tid,%cpu | grep 进程id。(用ps命令进一步定位是哪个线程因其的cpu占用过高)
jstack 进程id,可以把这个进程中所有java线程列出来,可以定位问题代码的源码行号。
程序运行很长时间没有结果
有可能是死锁问题
// 死锁问题
static A a = new A();
static B b = new B();
public static void main(String[] args){
new Thread(()->{
synchronized(a){
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace;
}
synchronized(b){
System.out.println("我获得了a和b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized(b){
synchronized(a){
System.out.println("我获得了a和b");
}
}
})
}
java是不能直接操作底层操作系统的,需要利用c/c++语言来操作,java中来调用其他语言的方法就是本地方法。
通过new关键字,创建对象都会使用堆内存。
特点:
它是线程共享的,堆中的对象都需要考虑线程安全的问题。
有垃圾回收机制。
public static void main(String[] args) {
int i = 0;
try{
List ls = new ArrayList();
String a = "hello";
while (true){
ls.add(a);
a += a;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
// ls的生命周期到catch之前,字符串不断添加到ls当中,最终会导致内存溢出
jps工具
查看当前系统中有哪些java进程。
jmap工具
查看堆内存占用情况。
jconsole
图形界面的,多功能的检测工具,可以连续检测。
jvirsualvm
图形界面的,展示虚拟机。
方法区是所有jvm线程共享的区域。
里面存储了一些和类有关的东西(类,类加载器,运行时常量池)。
概念上是堆的一部分,不同jvm厂商实现可能不同。(Oracle公司的hotspot在1.6之前是用永久代实现的,不属于堆;1.8之后用元空间实现,属于本地内存的一部分,而常量池被移到堆当中)。
1.6之前会导致永久代内存溢出,1.8之后会导致元空间内存溢出。
public class MethodDemo extends ClassLoader{
public static void main(String[] args) {
int j = 0;
try{
MethodDemo md = new MethodDemo();
for (int i = 0; i < 10000; i++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号,public,类名,包名,父类,接口
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
md.defineClass("Class"+i,code,0, code.length);
j++;
}
}finally {
System.out.println(j);
}
}
}
// 元空间使用的是本地内存,很大,一般不会内存溢出,演示时需要修改元空间大小
// -XX:MaxMetaspaceSize=8m
常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息。
运行时常量池。常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
对于数字来说,小于short整数类型的最大值,则该数字与字节码指令存储在一起(存储在方法区)。若超过short整数类型的最大值,则存入常量池。short整数最大值是32767。
常量池中的字符串仅仅是符号,第一次用到时才变为对象。
利用串池的机制,来避免重复创建字符串对象。
字符串变量的拼接原理时concat(jdk11),new 出来一个新的String对象,存放在方法区。
字符串常量拼接的原理是编译期优化。
可以使用intern方法,主动将串池中还没有的字符串对象放入串池。
(1.8)若串池中没有,主动将字符串放入串池,并返回串池中的字符串;若串池当中有该字符串,则不会放入串池,但是也会返回串池中的字符串。
(1.6)若串池中没有,会将此字符串复制一份,放入串池,并返回串池中的字符串;若串池当中有此字符串,则不会放入串池,但是也会返回串池中的字符串。
package com.kun;
/**
* 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
* @author wsk
* @version 1.0.0
* 2023/3/13 14:43
*/
// StringTable ["a","b","ab"],hashtable结构,不能扩容
public class HelloWorld {
// 常量池最初存在于类的二进制字节码文件当中
// 常量池中的信息都会被加载到运行时常量池中,这时a,b,ab都是常量池中的符号,还没变成Java变量
// ldc #2 会把 a 符号变为 "a" 字符串对象,并放入到StringTable当中
// ldc #3 会把 b 符号变为 "b" 字符串对象,并放入到StringTable当中
// ldc #4 会把 c 符号变为 "c" 字符串对象,并放入到StringTable当中
public static void main(String[] args) {
System.out.println("hello world");
String s1 = "a";
String s2 = "b";
String s5 = "ab";
String s3 = "a" + "b"; // javac在编译期间的优化,结果已经在编译期确定为“ab”
String s4 = s1 + s2; // (jdk1.8)其实创建了StringBuilder对象,调用append()方法,在heap中
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s2); // false
System.out.println(s4 == s5); // false
String x2 = new String("c") + new String("b");
String x1 = "cd";
x2.intern();
// 问,如果调换最后两行代码位置,结果有什么不一样?// true //jdk1.6 false
System.out.println(x2 == x1); // false
}
}
1.6版本,StringTable是常量池的一部分,随常量池存储在方法区。
1.7版本后,StringTable转移到了堆当中。
StringTable中存在垃圾回收
StringTable使用哈希表实现的,哈希表性能受限于哈希表的大小。
-XX:StringTableSize = 设定值,适当的增大设定值可以提高性能
考虑将字符串对象是否加入串池,因为串池中不能存储重复元素。(利用intern方法)
常见于NIO操作,用于数据缓冲区。
不接受jvm内存回收管理,属于操作系统内存,通过借用java中的虚引用,利用Unsafe对象的方法来释放内存。
分配回收成本较高,但读写性能高。
也有内存溢出的现象
没有使用Direct Buffer
使用了Direct Buffer
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。
jvm调优时,会利用-XX:+DisableExplictGC,禁用显示内存回收,也就是使得System.gc()方法失效,因为该方法时Full GC,比较耗费时间,但是这会导致直接内存长时间得不到释放。可以使用unsafe.freeMemory
引用计数法。(缺点,循环引用)
可达性分析算法
java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
哪些对象可以作为GC Root?(例如当前活动线程中,局部变量所引用的对象可以作为根对象),利用Memory Analyzer可以图形化显示当前根对象
只有没有GC Root对象引用他时,才会被垃圾回收
当软引用所引用的对象没有被GC Root对象引用时,若此时发生垃圾回收,并且回收完后内存还不够,则软引用所引用的对象会被释放掉。若配有相应的引用队列,软引用所引用的对象被回收后,则软引用会进入队列。此举是为了方便将弱引用占用的内存释放掉,因为他自身还被强引用所引用。
/**
* 演示软引用
* -Xmx20m,设置heap内存大小为20MB
*/
public class Soft {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
public static void soft() {
List> list = new ArrayList<>();
// 引用队列
ReferenceQueue queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联引用队列
SoftReference ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
Reference extends byte[]> poll = queue.poll();
while (poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("----------------------------");
System.out.println("循环结束:" + list.size());
for (SoftReference ref : list) {
System.out.println(ref.get());
}
}
}
当软弱引用所引用的对象没有被GC Root对象引用时,若此时发生垃圾回收,则弱引用所引用的对象会被释放掉(不管垃圾回收完内存是否充足)。若配有相应的引用队列,弱引用所引用的对象被回收后,则弱引用会进入队列。此举是为了方便将弱引用占用的内存释放掉,因为他自身还被强引用所引用。
public class Weak {
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
WeakReference ref = new WeakReference;
list.add(ref);
}
}
}
必须配合引用队列来使用。当使用直接内存时,会创建一个虚引用对象,并将直接内存地址传递给该虚引用对象。当byteBuffer被回收时,虚引用对象会进入引用队列,此时referenceHandler线程会定时的到引用队列里面找虚引用,并且会调用虚引用的clean()方法然后间接调用Unsafe.freeMemory()方法来释放直接内存。
若一个对象重写了Object父类的finallize()方法时,当没有强引用引用该对象时,此时jvm会创建一个终结器引用引用该对象。当该对象被垃圾回收时,会先把终结器引用加入引用队列,再由一个线程定期查看该队列是否由终结器引用,并根据终结器引用找到作为垃圾回收的对象,并且调用finallize()方法,当再次(第二次垃圾回收)垃圾回收时,这个对象就会被回收。
其中实线代表强引用。
先标记不被GC Root所强引用的对象的地址,然后再清除,会导致内存空间不连续。(优点:清除速度快;缺点:容易产生内存碎片)。
先标记不被GC Root所强引用的对象的地址,然后再将不连续的内存空间整理成连续的内存空间。(优点:无内存碎片;缺点:整理牵扯到了对象的移动,速度较慢)。
先将内存区域划分成两块大小相等的区域,一个是from区域,一个时to区域(始终空闲,没有对象)。先标记不被GC Root所强引用的对象的地址,然后将这些对象复制到to区域,复制的过程中会完成内存的碎片整理。此时将from区域全部清除,最后交换from和to区域的位置。(优点:无内存碎片;缺点:会占用双倍的内存空间)。
jvm结合上面多种算法共同实现垃圾回收。
堆中的对象可以分为新生代和老年代。长时间使用的对象放在老年代,用完了就可以丢弃的对象放在新生代当中。这样就可以根据不同代的特点进行垃圾回收。
第一次垃圾回收:刚创建出来的对象会被放入伊甸园当中,当伊甸园空间不够时,会触发一个新生代的垃圾回收(Minor GC)。这里会采用复制算法,先标记那些要清理的对象,然后将存活的对象复制到幸村区(to区域),并将这些幸存的对象的寿命+1,最后交换幸存区(from)和幸存区(to)的位置。伊甸园的那些对象就会被回收掉。
Minor GC会引发stop the world,暂停其他用户的线程,垃圾回收结束,用户线程才恢复运行。
第二次垃圾回收,同时判断伊甸园和幸存区中的对象是否需要被回收,然后会将两个区域存活的对象寿命+1放入幸存区(to)当中,将伊甸园和幸存区(from)中的对象进行垃圾回收,最后再交换幸存区(from)和幸存区(to)的位置。
当幸存区(from)中的对象经过一定次数的垃圾回收,仍然存活,此时寿命达到阈值(15),会将其放入老年代当中。
当老年代空间不足,会先尝试一次Minor GC,如果之后空间仍然不足,那么触发full GC。full GC引发的stop the world时间更长。
-Xms,初始堆大小
-Xmx或-XX:MaxHeapSize=size,堆最大大小
-Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size),新生代大小
-XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy,幸存区比例(动态)
-XX:SurvivorRatio=ratio,幸存区比例
-XX:MaxTenuringThreshold=threshold,晋升阈值
-XX:+PrintTenuringDistribution,晋升详情
-XX:+PrintGCDetails -verbose:gc,GC详情
-XX:+ScavengeBeforeFullGC,Full GC前Minor GC
-XX:+UseSeriaGC=Serial (工作再新生代,采用复制算法)+ SerialOld(工作在老年代,采用标记整理算法)
底层是单线程(一个线程)的垃圾回收器,当发生垃圾回收时,其他线程都暂停。
适用堆内存较小的时候,适合个人电脑。
-XX:+UseParrallelGC ~(工作在新生代,复制算法) -XX:+UseParrallelOldGC(工作在老年代,标记整理算法)。
-XX:ParrallelGCThreads=n,控制线程数。
-XX:+UseAdaptiveSizePolicy,采用自适应策略调整新生代大小。
-XX:GCTimeRatio=ratio,调整吞吐量(垃圾回收时间跟总时间的占比)。垃圾回收时间占比公式:1/(1+ratio)。
-XX:MaxGCPauseMillis=ms,最大暂停毫秒数(默认200ms)。
多线程的,用户线程到达安全区后,先暂停,多个垃圾回收线程先执行垃圾回收,垃圾回收完成后,用户线程再继续执行。
适合堆内存较大的,需要多核CPU来支持。
让单位时间内stop the world时间最短,也就是垃圾回收时间占程序运行时间的占比尽可能低。
-XX:+UseConcMarkSweepGC(工作在老年代,基于标记清除算法) ~ -XX:+UseParNewGC (工作在新生代,基于复制算法)~ SerialOld(若前面CMS垃圾回收器发生故障,退化为串行回收器,来利用标记整理算法做一次内存碎片的整理)。
-XX:ParrallelGCThreads=n(并行GC线程数一般和CPU核数一致) ~ -XX:ConcGCThreads=threads(并发GC线程数,一般设置为n的四分之一)。
-XX:CMSInitiatingOccupancyFraction=precent,执行CMS垃圾回收时的内存占比,设置越小,执行越频繁。
-XX:+CMSScavengeBeforeMark,在重新标记之前,先做一次垃圾回收。
多线程,在某些阶段用户线程和垃圾回收线程并发执行。
适合堆内存较大的,需要多核CPU来支持。
尽可能让单次Stop the world时间最短。
2017年,JDK9默认垃圾回收器
同时注重吞吐量(Throughput)和低延迟(Low Latency),默认暂停目标是200ms。
超大堆内存,会将堆划分为多个大小相等的Region。
整体上是标记+整理算法,两个区域之间是复制算法。
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
Young Collection -> Young Collection + Concurrent Mark -> Mixed Collection -> Young Collection。
-XX:+UseStringDeduplication
优点:节省了大量内存。
缺点:略微占多了cpu时间,新生代回收时间略微增加。
String s1 = new String("Hello"); // char[]{'H','e','l','l','o'}
String s2 = new String("Hello"); // char[]{'H','e','l','l','o'}
G1会将所有的字符串放入一个队列。
当新生代回收时,G1并发检查是否由重复字符串。
如果它们值一样,让它们引用同一个char[]
注意,与String.intern()不一样,
String.intern()关注的是字符串对象
而字符串去重关注的是char[]
它们在jvm内部,使用了不同的字符串表
所有对象经过并发标记之后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark(默认启用)。
一个对象大于region的一半时,称之为巨型对象。
G1不会对巨型对象进行拷贝。
回收时被优先考虑。
G1会跟踪老年代所有incoming引用,这样老年代的incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。
并发标记必须在堆空间被占满前完成,否则会触发Full GC
JDK9之前需要使用:-XX:IniatiatingHeapOccupancyPercent
JDK9之后可以动态调整:
-XX:IniatiatingHeapOccupancyPercent用来设置初始值。
进行数据采样并动态调整阈值,可以增大或者减小
总会添加一个安全的空挡空间
新生代内存不足发生的垃圾回收,Minor GC
老年代内存不足发生的垃圾回收,Full GC
新生代内存不足发生的垃圾回收,Minor GC
老年代内存不足发生的垃圾回收,Full GC
新生代内存不足发生的垃圾回收,Minor GC
老年代内存不足,当老年代内存占比堆内存达到一个阈值时才会触发垃圾回收,但是如果并发回收垃圾的速度大于新垃圾产生的速度(并发失败退回为SerialOld),也不会触发Full GC。
新生代内存不足发生的垃圾回收,Minor GC
老年代内存不足,当老年代内存占比堆内存达到一个阈值时才会触发垃圾回收,但是如果并发回收垃圾的速度大于新垃圾产生的速度(并发失败退回为SerialOld),也不会触发Full GC。
首先确定调优的目标是低延迟还是高吞吐量,选择合适的回收器。
高吞吐量回收器:ParallelGC
低延迟回收器:CMS;G1; ZGC
查看FullGC前后的内存占用,考虑下面几个问题:
数据是不是太多?
例如:resultSet = statement.executeQuery("select * from 大表")
优化,加limit:resultSet = statement.executeQuery("select * from 大表 limit")
数据表示是不是太臃肿?
对象图 。不一定把一个对象所有的东西都查出来,用到什么查什么
对象大小。 java最小的对象16个字节,能用基本类型就不用包装类型
是否存在内存泄露
例如:static Map map;频繁的向map里面放入数据又不移除。
优化:长时间存活的对象建议用软引用或者弱引用;使用第三方的缓存实现(redis等)
新生代特点
所有的new操作的内存分配非常廉价。
TLAB thread-local allocation buffer:每个线程都在伊甸园有一部分私有的区域。
进行new操作时会先检查TLAB里面有没有相应的内存,多线程同时创建对象时不会出现并发安全问题
死亡对象的回收代价是零。
因为采用的是复制拷贝算法。
大部分对象用过即死。
Minor GC的时间远低于Full GC。
新生代内存大小
若新生代内存太小,容易发生大量的Minor GC,stop the world较多
若新生代内存太大,对应的老年代内存会较少,容易发生Full GC,stop the world时间变得更长。官方建议新生代内存大小为堆内存的1/4到1/2。
新生代能容纳{并发量*(请求和响应过程中所创建的对象占用的内存大小)}
幸存区大小
幸存区需要大到能够保留【当前活跃对象+需要晋升对象】
若幸存区太小,jvm会动态调晋升阈值,可能会将一些存活时间较短的对象晋升到老年代。这样会导致这些存活时间较短的对象只能等到老年代的垃圾回收时才会被清理掉。
以CMS为例:
CMS老年代的内存越大越好。
先不尝试做调优,如果没有Full GC那么已经可以了,否则先尝试新生代调优。
观察Full GC时老年代的内存占用,将老年代的内存预设调大1/4 到1/3。
-XX:CMSInitiatingOccupancyFration=percent,当老年代的空间占用达到老年代总内存的percent时,会发生一次垃圾回收。一般75%-80%。
Full GC和Minor GC发生频繁
调大新生代内存,增加幸存区空间和晋升阈值。
请求高峰期发生Full GC,单次暂停时间特别长(CMS)
重新标记占用时间较长,在重新标记开始前先做一次Minor GC。
/**
* 执行对应的字节码指令为iinc,直接在局部变量slot上进行运算
* a++和++a的区别是先执行iload还是先执行iinc
* a++是先执行iload然后执行iinc,iload(将变量从局部变量表加载到操作数栈),iinc也是操作的局部变量表
* ++a是先执行iinc然后执行iload
*/
public class TestAPlus{
public static void main(String[] args){
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a); // 11
System.out.println(b); // 34
}
}
while循环和for循环,虽然语法不一样,但是编译成字节码之后,二者是一样的。
public class Test{
public static void main(String[] args){
int i = 0;
int x = 0;
while(i < 10){
x = x++;
i++;
}
System.out.println(x); // 0
}
}
// 编译器会按照从上到下的顺序,收集所有static静态代码块和静态成员赋值的代码。
// 合并为一个特殊的方法()V,()V方法会在类加载的初始化阶段被调用。
// 最终结果 i=30
public class Test{
public static void main(String[] args){
static int i = 10;
static{
i = 20;
}
static{
i = 30;
}
}
}
// 编译器会按照从上到下的顺序,收集所有{}代码块和成员变量赋值的代码。
// 形成新的构造方法,但原始构造方法内的代码总是在最后。
public class Test{
private String a = "s1";
{
b=20;
}
private int b = 10;
{
a = "s2";
}
public Test(String a,int b){
this.a = a;
this.b = b;
}
public static void main(String[] args){
Test t1 = new Test("s3",30);
System.out.println(t1.a); // s3
System.out.println(t1.b); // 30
}
}
不要通过对象来调静态方法,建议直接用类名来调用静态方法,否则会产生两条不必要的虚拟机指令。
public class Test{
public static void main(String[] args){
int result = test();
System.out.println(result); // result:20
}
public static int test(){
try{
return 10;
}finally{
return 20;
}
}
}
// 若在finally中写了return,则不会抛出异常,因为没有athrow字节码指令了
public class Test{
public static void main(String[] args){
int result = test();
System.out.println(result); // result:10
}
public static int test(){
int i = 10;
try{
return i;
}finally{
i = 20;
}
}
}
// 若在try块里面做了return,在finally块里面做了修改,那么修改是无用的,
// 因为在修改前做了istore_1,目的是为了固定返回值。
所谓语法糖就是java编译器把*.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员负担(给糖吃嘛)。
语法糖举例:
默认构造器,java源码不写构造器,自动给你加上默认构造器。super()
自动拆装箱。
泛型擦除。
泛型反射。
可变参数。
foreach循环。(数组其实就是按照下标进行遍历,集合就是利用迭代器)
switch,switch可以作用于字符串和枚举类。
try with resource
加载
验证
准备:为static变量分配空间,设置默认值。
static变量在JDK7之前存储与instanceKlass末尾,从JDK7开始,存储在_java_mirror末尾。
static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。
如果static变量是final类型的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成。
如果static变量是final类型的,但属于引用类型,那么赋值也会在初始化阶段完成。
解析:将常量池中的符号引用解析为直接引用。
初始化:调用
// 使用a,b,c三个常量是否会导致E初始化
public class Load4P{
public static void main(String[] args){
System.out.println(E.a); // 不会
System.out.println(E.b); // 不会
System.out.println(E.c); // 会
}
}
class E{
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;
}