Java基本功-类加载机制与内存分析

类加载器分类,动态创建java对象,jvm结构,垃圾回收机制,优化

1. 动态编译

1
2
3
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//int javax.tools.Tool.run(InputStream in, OutputStream out, OutputStream err, String... arguments)
int result=compiler.run(null, null, null, "/A.java");

2. 类加载过程

2.1. 类加载

程序通过javac命令以后,生成一个或多个字节码文件。使用java命令对字节码文件进行解释运行。

将类的class文件读入内存,并为之创建一个java.lang.Class对象

类加载由类加载器来完成,类加载器由jvm提供

可以通过extends ClassLoader实现自定义类加载器

2.2. 连接

把类的二进制数据合并到JRE中

验证

检验被加载的类是否有正确的内部结构,与其他类协调一致

准备

为类的变量分配内存,设置初始值

解析

将类的二进制数据中的符号引用替换成直接引用

2.3. 初始化

初始化类变量

声明类变量时指定初始值

使用静态初始化块为类变量指定初始值

2.3.1. 类主动引用-一定发生类初始化

创建类实例 new

调用类的静态成员(除了final常量)和静态方法

使用java.lang.reflect包的方法对类进行反射调用

虚拟机启动,java Hello,一定会初始化Hello类,启动main方法所在的类

初始化类先初始化父类

###类被动引用-不会发生类初始化

访问静态域时,只有真正声明这个域的类才会被初始化

子类引用父类的静态变量,不会导致子类的初始化

通过数组定义类引用,不会触发类的初始化

引用常量不会触发此类的初始化,常量在编译阶段就存入调用类的常量池中

3. Java初始化顺序

  1. 父类静态变量,只初始化一次
  2. 父类静态代码块
  3. 子类静态变量
  4. 子类静态代码块
  5. 父类非静态变量
  6. 父类非静态代码块
  7. 父类构造函数
  8. 子类非静态变量
  9. 子类非静态代码块
  10. 子类构造函数

4. 类加载器

类用 【包名+类名+类加载器】 作为唯一标识

4.1. Bootstrap ClassLoader 根类加载器

加载java核心类。由jvm自身实现。String ,System这些核心类库在/jre/lib/rt.jar中

并不继承java.lang.ClassLoader

用原生代码C++实现

加载扩展类和应用程序类加载器,并指定他们的父类加载器

4.2. Extension ClassLoader 扩展类加载器

ClassLoader extension=system.getParent(); //ExtClassLoader

加载JRE扩展目录中jar包类 /jre/lib/ext

4.3. System ClassLoader 系统类加载器

ClassLoader system=ClassLoader.getSystemClassLoader(); //AppClassLoader

加载环境变量所指定的类路径。

4.4. URLClassLoader类

作为SystemClassLoader,ExtensionClassLoader的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ClassLoader classLoader = SimpleTests.class.getClassLoader();
InputStream in = classLoader.getResourceAsStream("db.properties");

Properties pro = new Properties();
pro.load(in);
String user = pro.getProperty("user");

//=============================================
URL[] urls = {new URL("file:mysql-connector-java-bin.jar")};
URLClassLoader loader = new URLClassLoader(urls);
Driver driver = (Driver)loader.loadClass("com.mysql.jdbc.Driver").newInstamce();

Properties p = new Properties();
p.setProperty("user",user);
p.setProperty("pwd",pwd);
Connection con = driver.connect(url,p);

4.5. 类加载机制

全盘负责

父类委托

缓存机制

保证所有加载过的class都被缓存,当缓存区中不存在class对象时,系统才读取class二进制文件,转换成class对象,存入缓存区。

4.6. 自定义类加载

1
2
3
4
5
6
extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{

}
}

案例

修改当前线程的类加载器

Thread.currentThread().setContextClassLoader(myclass);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@Test
void testSystem() throws Exception {
String path = System.getProperty("user.dir")+"/src/test/java/";
MyClass myclass = new MyClass(path);
Class<?> c = myclass.loadClass("com.runaccepted.online.admin.SimpleTests");
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(c.getClassLoader());

Thread.currentThread().setContextClassLoader(myclass);
//com.runaccepted.online.admin.MyClass@49e202ad
System.out.println(Thread.currentThread().getContextClassLoader());

}


public class MyClass extends ClassLoader{

String rootDir;
public MyClass(String rootDir){
this.rootDir = rootDir;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c!=null){
return c;
}else{
//双亲委派机制
ClassLoader parent = this.getParent();
try{
c = parent.loadClass(name);
}catch (Exception e) {
e.printStackTrace();
}
if (c!=null){
return c;
}else{
byte[] data = getData(name);
if(data==null){
throw new ClassNotFoundException();
}
else{
c = defineClass(name,data,0,data.length);
return c;
}

}

}
}
private byte[] getData(String name){
//com.runaccepted.online.User -> com/runaccepted/online/User.class
String path = rootDir + "/"+ name.replace(".","/")+".class";
ByteArrayOutputStream out = null;
InputStream in = null;
try{
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while((len=in.read(buffer))!=-1){
out.write(buffer,0,len);
}
return out.toByteArray();

}catch(Exception e){
e.printStackTrace();
return null;
}finally{

try {
if(out!=null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(in!=null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

}
}

编译java文件方法

1
2
3
4
5
6
7
8
9
10
private boolean compile(String javaFile) throws IOException{
Process p = Runtime.getRuntime().exec("javac "+javaFile);
try{
p.waitFor();
}catch(InterruptedException e){
System.out.println(e);
}
int ret = p.exitValue();
return ret==0;
}

5. JVM结构

5.1. 类加载子系统

负责从文件系统或者网络中加载class信息,加载的类信息存放于一块称为方法区的内存空间中。

5.2. 方法区

从属于堆

存放类相关信息和运行时常量池信息:

  1. 代码
  2. 静态变量
  3. 静态方法
  4. 字符串常量

5.3. 堆

java堆在虚拟机启动时建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的,这是一块与java应用密切相关的内存空间。

拥有不连续的空间,分配灵活

5.3.1. 堆内存为什么分代?

程序所有对象实例都存放在堆中,堆作为垃圾回收最频繁的一块区域,给堆分代是为了提高对象内存分配和垃圾回收的效率。避免了所有新创建的对象和生命周期很长的对象在每次回收扫描中都必须被一同遍历,浪费时间。新创建的对象分配在新生代,经过多次回收算法扫描后依旧存活的存入老年代,静态属性,类信息等存放在永久代中,新生代存活时间短,会被频繁gc垃圾回收,老年代则频率相对较低,永久代一般就不进行垃圾回收。

5.3.2. 内存分代划分

新生代,老年代,永久代。永久代是HotSpot虚拟机特有的概念,采用永久代的方法实现方法区。在jdk1.7中HotSpot已经开始去永久代,把字符串常量池移出。永久代中存放常量,类信息,静态变量等数据。新生代和老年代是垃圾回收的主要区域。

5.3.3. 新生代

新生成的对象优先存放在新生代中,新生代对象存活率很低,进行一次垃圾收集会回收70%–95%的空间。

HotSpot将新生代划分为三块,一块为较大的Eden空间和两块较小的Survivor空间from和to,比例为8:1:1.划分的目的为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生代的对象在Eden区分配(大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次MInor GC。GC开始时,对象只存于Eden和From Survivor区,To Survivor区是空的。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,在From Survivor区中,仍然存活的对象每熬过一轮回收年龄+1(对象年龄存储在header中),最后会根据它们的年龄值决定去向,年龄值达到一定的阀值会被移入老年代,没有达到阀值的被复制到To Survivor区中。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区中。接着,From和To区互换角色,即To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

5.3.4. 老年代

在新生代中经历多次GC后仍然存活的对象进入老年代。老年代的生命周期较长,存活率较高,回收速度较慢。

5.3.5. 永久代

永久代存储类信息,常量,静态变量,即时编译器编译后的代码等数据,对该区域,java虚拟机规范指出可以不进行垃圾收集,一般不就不进行垃圾回收。

5.4. 直接内存

java的NIO库允许java程序使用直接内存。直接内存是在java堆外的。直接向系统申请内存空间。通常访问直接内存的速度优于堆。读写频繁的场合可能会考虑使用直接内存。直接内存位于堆外,故它的大小不受Xmx指定,但系统内存有限,堆和直接内存的总和依然受限于操作系统能给的最大内存。

5.5. 垃圾回收机制

垃圾回收机制可对方法区,堆和直接内存进行回收。其中,主要是java堆。和c/c++不同,java中所有的对象空间释放都是隐式的,java中没有所谓的free()/delete()来指定释放内存区域。垃圾回收机制在后台默默工作,标识并释放垃圾对象,完成包括java堆,方法区,直接内存的全自动管理。

5.6. Java栈

方法执行的内存模型,方法被调用时会为方法开辟栈帧

存放局部变量,方法参数

由系统自动分配,拥有连续的内存空间

5.7. 本地方法栈

和java栈类似,java栈用于方法的调用,本地方法栈用于本地方法的调用,本地方法一般用C语言编写

5.8. PC

Program Counter

pc寄存器是每个线程私有的空间,java虚拟机会为每一个java线程创建pc寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。pc寄存器会指向当前方法中正在执行的指令。如果方法为本地方法,则pc=undefined。

5.9. 执行引擎

负责执行虚拟机的字节码

为了提高虚拟机执行效率,会使用即时编译技术(just in time)将方法编译成功后机器码后再执行

6. 垃圾回收算法

6.1. 引用计数

对象有一个引用,计数+1,删除一个引用,计数-1,垃圾收集计数为0的对象。

无法处理循环引用

6.2. 复制copying

把内存空间划分为两个相等的区域,每次使用一个区域,垃圾回收时,遍历当时使用的区域,把对象复制到另一个区域中。算法成本小,复制过去的对象不会有内存碎片问题,但需要两倍内存空间。

6.3. 标记-清除mark-sweep

此算法执行分为两阶段。第一个阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。算法执行时需要暂停整个应用,同时产生内存碎片。

7. 垃圾收集器

7.1. Scavenge GC

新生代GC指发生在新生代的GC,新生代对象朝生夕死,所以Scavenge GC非常频繁,回收速度比较快。Eden空间不足以为对象分配内存时,会触发Scavenge GC。

7.2. Full GC

老年代GC指发生在老年代的GC,出现了Full GC一般会伴随至少一次的Minor GC(从新生代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。

8. 分代垃圾回收器

8.1. Serial串行收集器

HotSpot运行在Client模式下的默认新生代收集器,串行收集器的特点是只用一个CPU/一条收集线程去完成GC工作,且在进行垃圾收集时必须暂停其他所有的工作线程(Stop The Word),可以使用-XX:+UseSerialGC打开

8.2. ParNew并行收集器

Serial的多线程版本,除使用多条线程进行GC外,包括Serial可用的所有控制参数,收集算法,STW,对象分配规划,回收策略等都与Serial完全一样(也是vm启用CMS收集器``-XX:+UserConcMarkSweepGC的默认新生代收集器)。可用-XX:parallelGCThreads=`参数控制GC线程数。

8.3. Parallel Scavenge收集器

与ParNew收集器类似,Parallel Scavenge也是使用复制算法,也是并行多线程收集器。但Parallel Scavenge更关注系统吞吐量。

系统吞吐量=运行用户代码时间(运行用户代码时间+垃圾收集时间)

停顿时间越短越适用于用户交互的程序,高吞吐量适用于后台运算而不需要太多交互的任务。

8.4. Serial Old收集器

Serial收集器的老年代版本,单线程收集器,使用标记-清理算法

8.5. Parallel Old收集器

Parallel Old是Parallel Scavenge的老年代版本。使用多线程和“标记-整理”算法,吞吐量优先,主要与Parallel Scavenge配合在注重吞吐量及CPU资源敏感系统内使用

8.6. CMS收集器

Concurrent Mark Sweep收集器是一款真正意义上的并发收集器,虽然有了理论意义上表现更好的G1收集器。

CMS收集器是一种获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现,整个GC过程分为4个步骤:

  1. 初始标记 CMS inital Mark 需STW
  2. 并发标记 CMS concurrent mark
  3. **重新标记 **CMS remark 需STW
  4. 并发清除 CMS concurrent sweep

8.7. java.lang.management

1
2
3
4
5
6
7
8
9
10
11
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;

@Test
void testSystem() throws Exception {
List<GarbageCollectorMXBean> list = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gm:list) {
System.out.println(gm.getName());
}

}

9. JVM优化

9.1. jvisualvm运行监控

/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/jvisualvm

9.2. JVM参数

-Xms 初始堆大小

-XMx 最大堆大小

-XX:NewSize=n设置年轻代大小

-XX:NewRatio=n 设置年轻代和年老代的比值。如=3,表示年轻代:老年代=1:3

-XX:+UseSerialGC 设置串行收集器

-XX:+UseParallelGC 设置并行收集器

本文结束  感谢您的阅读