ZhangWenL

Hello world,hello java


  • 首页

  • 分类

  • 归档

使用scrapyd部署scrapy爬虫

发表于 2018-06-28 | 分类于 python

之前运行爬虫都是直接通过命令行启动,但当爬虫部署到服务器上再通过命令行启动会十分不方便。最好能够直接在网页上进行操作,scrapyd库正好可以满足该需求,使用scrapyd可以接通过http控制爬虫的启动和停止,且可以同时管理多个爬虫,每个爬虫可以拥有多个版本。

安装

安装scrapyd:pip install scrapyd

安装打包工具scrapyd-client:pip install scrapyd-client

部署项目

1.修改scrapy.cfg文件

默认配置:

1
2
3
[deploy]
#url = http://localhost:6800/
project = Spider

deploy节点可配置参数如下:

  • url:url为scrapyd服务器的地址
  • project:project为项目名称,可随便定义
  • version:默认会根据当前时间戳生成项目的版本信息,如需自定义则在最后加上version参数
  • target:deploy后面可加上target名称,指定不同的scrapyd服务器

修改后的scrapy.cfg如下:

1
2
3
4
[deploy:demo]
url = http://localhost:6800/
project = Spider
version = 1.1

2.查看所有配置

使用scrapyd-deploy -l查看所有配置项

windows下可能会提示 ‘scrapyd-deploy’ 不是内部或外部命令。

打开python安装目录下的Scripts文件夹,新建scrapyd-deploy.bat文件,将如下内容填入该bat文件(注意替换成自己的目录)

@echo off

“D:\Developer Tools\python\python.exe” “D:\Developer Tools\python\Scripts\scrapyd-deploy” %1 %2 %3 %4 %5 %6 %7 %8 %9

3.部署

使用scrapyd命令启动scrapyd服务器,cd到scrapy项目根目录,使用如下命令部署指定项目到指定的scrapyd服务器中:

1
scrapyd-deploy <target> -p <project>

如果一个项目需要部署到多个target,可使用如下命令:

1
scrapyd-deploy -a -p <project>

返回如下信息表示部署成功:

1
2
3
Deploying to project "Spider" in http://localhost:6800/addversion.json
Server response (200):
{"node_name": "USER-U2VF9HUJ65", "status": "ok", "project": "Spider", "version": "1.0", "spiders": 4}

API

  • 查看服务器状态(get)
    • http://localhost:6800/daemonstatus.json
  • 上传的项目列表(get)
    • http://localhost:6800/listprojects.json
  • 获取某一项目所有版本列表(get)
    • http://localhost:6800/listversions.json?project=Spider
  • 获取项目某一版本下所有的爬虫(get)
    • http://localhost:6800/listspiders.json?project=Spider&version=1530149345
  • 获取所有的jobs(get)
    • http://localhost:6800/listjobs.json?project=Spider
  • 删除某一版本(post)
    • http://localhost:6800/delversion.json
    • 参数:project 项目名称,version 项目版本
  • 删除项目(post)
    • http://localhost:6800/delproject.json
    • 参数:project 项目名称
  • 开启爬虫(post)
    • http://localhost:6800/schedule.json
    • 参数:project 项目名称,spider 爬虫名称
    • 返回job id,停止爬虫时需要该参数
  • 取消爬虫(post)
    • http://localhost:6800/cancel.json
    • 参数:project 项目名称,job 启动时返回的id

参考

  • https://github.com/scrapy/scrapyd-client
  • http://scrapyd.readthedocs.io/en/stable/index.html

解决防盗链导致图片无法引用的问题

发表于 2018-06-11 | 分类于 其它

很多网站都有反盗链机制,当我们直接把对方的图片地址贴到我们的页面上是无法显示的,比如微信公众号的图片:

image

这是由于浏览器在发起请求的会自动在请求头里添加referer,显示该请求来自哪个页面。对方服务器会对referer进行判断,来自其他域名的请求会被屏蔽。而直接通过浏览器打开的请求则不会添加referer,为了用户正常使用,一般不会屏蔽空referer的请求,因此我们只需要在请求中想办法去掉referer即可正常显示图片。以下两个是我亲测可行的方案。

HTML页面添加meta标签

在需要去除referer的页面中添加<meta name="referrer" content="never">,注意name为referrer不是referer,将content设为never,则浏览器在发起请求前会删除http请求head中的referer。关于referer更多设置的值可以参考该文章:《使用 Referer Meta 标签控制 referer》

服务器转发请求

上面方法比较简单直接,但并不是所有时候都适用,比如在邮件中则无法显示,因为邮件服务商一般都会删除邮件中的标签。

该方法是将原图片的url作为参数传到服务器,由服务器设置referer为null,并302重定向到原来的url。

服务器端的代码如下:

1
2
3
4
5
6
@RequestMapping("noreferer")
public void noreferer(HttpServletResponse response, String url){
response.addHeader("location",url);
response.addHeader("referer",null);
response.setStatus(302);
}

修改html页面的img标签,如:<img src="http://原图片url"> 改为:<img src="http:你服务器地址/noreferer?url=http://原图片url"

创建线程池

发表于 2018-05-10 | 分类于 Java

使用Executors创建线程池

Java 1.5引入了Executors API,通过该API可以直接创建线程池。

1
2
3
4
5
6
7
8
9
10
11
//创建一个可重用的线程池,任务会选择线程池中已创建的线程,如果没有可用线程,则会在线程池中创建新线程,如果线程60秒内没被使用,将被关闭并移除
Executors.newCachedThreadPool();

//创建一个固定大小的线程池,支持定时以及周期任务
Executors.newScheduledThreadPool(10);

//创建单个工作线程的Executor,所有提交的任务会按照提交的顺序依次执行
Executors.newSingleThreadExecutor();

//创建固定长度的线程池,任何时候最多只会创建指定个数的线程来执行任务,如果提交任务时没有可用线程,该任务将会的队列中等待,知道线程可用
Executors.newFixedThreadPool(10);

手动创建线程池

虽然通过Executors API可以很方便的创建线程池, 但是阿里巴巴Java开发手册中确是不允许这么做的,提示我们Executors存在OOM的风险,应使用ThreadPoolExecutor手动创建线程池。

mark

ThreadPoolExecutor构造函数如下:

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//....
}
  • corePoolSize:核心线程池数量,除非通过allowCoreThreadTimeOut()设置超时,否则无论线程是否闲置,线程池中都将保留该数量的线程
  • maximumPoolSize:线程池所允许线程的最大数量
  • keepAliveTime:存活时间,当线程池内的线程数量超过corePoolSize时,闲置线程等待的时间。超过该时间线程将被关闭
  • unit:keepAliveTime的单位
  • workQueue:存放未处理的任务队列
  • ThreadFactory:executor创建新线程时使用的ThreadFactory
  • RejectedExecutionHandler:当线程都在使用且队列内等待的任务数量达到队列上限时调用

线程池核心以及最大数量

ThreadPoolExecutor会根据corePoolSize和maximumPoolSize自动调整线程池大小,当提交新任务时,若当前线程数小于corePoolSize,无论是否有闲置线程都将创建新的线程。如果线程数介于corePoolSize和maximumPoolSize之间,只有不存在闲置线程时才会创建新线程。

预先启动核心线程

默认情况下,ThreadPoolExecutor只会创建线程,只有通过execute()提交任务时才会启动线程。如果参数传入的为非空队列,可通过prestartCoreThread()直接启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();

Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
};

queue.add(runnable);

ThreadPoolExecutor executor = new ThreadPoolExecutor(1,5,60,TimeUnit.SECONDS,queue);

executor.prestartCoreThread();//注释掉该行上面的runnable不会执行
executor.shutdown();

创建新线程

线程池通过参数中传入的ThreadFactory创建新线程,默认为Executors.defaultThreadFactory(),使用自定义ThreadFactory可以为线程指定有意义的名称,方便出错时定位,还可以设置线程优先级,设置为守护线程等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();

AtomicInteger i = new AtomicInteger(1);

//自定义ThreadFactory
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(Thread.currentThread().getThreadGroup(),r,"my_thread_" + i.getAndIncrement(),0);
return thread;
}
};

ThreadPoolExecutor executor = new ThreadPoolExecutor(1,5,5,
TimeUnit.SECONDS,queue,threadFactory);

executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});

executor.shutdown();

阻塞队列

BlockingQueue用来存放提交的任务,根据任务数量的不同分为以下三种使用情况:

  • 如果运行的线程数少于corePoolSize,executor会优先创建新的线程,而非加入队列
  • 若运行的线程数大于corePoolSize,executor会优先将任务加入队列
  • 如果队列已满,则会创建新的线程,若线程数量大于maximumPoolSize,该任务会被拒绝

拒绝策略

如果Executor被关闭或者队列已满且线程数量已达maximumPoolSize,则通过execute()提交的任务会被拒绝。发生上述情况会调用RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)。预定义的处理策略有如下四种:

  • ThreadPoolExecutor.AbortPolicy:默认值,抛出一个运行时异常RejectedExecutionException
  • ThreadPoolExecutor.CallerRunsPolicy: 直接在调用 execute 方法的线程执行该任务
  • ThreadPoolExecutor.DiscardPolicy:任务无法执行直接丢弃
  • ThreadPoolExecutor.DiscardOldestPolicy:若 executor 没有关闭,队列前面的任务会被丢弃,然后重试(如果失败重复上步)

连接远程服务器mysql

发表于 2018-05-02 | 分类于 mysql

mysql默认只可以本地访问,需要修改user表使其允许所有ip进行访问

1
mysql -u name -p

登录数据库

1
use mysql;

切换到mysql数据库

1
select host,user from user;

可以看到root用户对应的host为localhost
paste image

1
update user set host="%" where user="root";

将用户对应的host改为%,表示可以从任何机器进行访问

修改防火墙

要远程连接需要确保3306端口没有被防火墙拦截

1
telnet 你的ip 3306

访问3306端口,如果访问失败,通过以下命令配置防火墙

1
ufw allow 3306

允许访问3306端口

1
ufw status

命令查看当前防火墙状态。

检查端口是否绑定在本地

若修改完防火墙telnet仍然失败,可通过以下命令查看端口状态

1
netstat -apn|grep 3306

显示

1
tcp6 0 0 127.0.0.1:3306 :::* LISTEN 13524/mysqld

说明3306端口被绑定在本地,修改/etc/mysql目录下的my.cnf文件,删除

1
bind-address=127.0.0.1

配置项,或将其改为0.0.0.0

Java内存模型

发表于 2018-04-10 | 分类于 JVM高级特性

Java内存模型

Java内存模型的主要作用是定义了程序中各个变量的访问规则。
paste image
程序中所有变量都保存在主内存中,每个线程都有各自的工作线程,线程无法直接操作主内存中的变量。当线程需要操作某个变量时先将其从主内存中拷贝到自己的工作内存,操作完毕在同步到主内存,每个线程无法操作其它线程的工作内存。

内存间交互

主内存和工作内存之间的交互有如下八个步骤:

  • lock(锁定):将主内存中的某个变量标记为线程独占状态
  • unlock(解锁):取消线程独占状态
  • read(读取):将主内存中的变量传输到工作内存
  • load(加载):将read得到的变量放到工作内存中的变量副本中
  • use(使用):将工作内存中的变量传给执行引擎
  • assign(赋值):将执行引擎返回的结果保存到工作内存中的变量副本
  • store(保存):将工作内存中的变量传输到主内存中
  • write(写入):将store过程的值保存到主内存的变量中

从主内存读取时,read和load一定是顺序执行的;保存到主内存时store和write也是顺序执行(顺序执行非连续执行,read和load中间可插入其他操作)

volatile关键字

volatile关键字是Java中最轻量级的同步机制,volatile关键字主要有以下两个作用:

  • 保证线程之间的可见性(无法保证原子性)
  • 禁止指令重排

之所以volatile能保证可见性是因为使用volatile修饰的变量每次修改后都会直接同步到主内存,每次使用前会直接从主内存之中读取,因此所有的修改对其他线程都是立即可见的。
由于volatile只保证可见性不保证原子性,因此使用时需满足以下两个条件:

  • 对变量的操作不依赖变量当前值
  • 改变了没有包含在具有其他变量的不变式中

关于指令重排序可参见美团点评技术团队的《Java内存访问重排序的研究》一文。

类加载机制

发表于 2018-03-29 | 分类于 JVM高级特性

类加载过程

类的加载过程包括加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备和解析合称为连接,加载过程如下图所示:
paste image
加载过程除解析以外其他步骤顺序都是确定的,为了支持java的动态绑定,解析阶段有可能在初始化之后执行。

加载

因为非数组类的加载可以使用自定义的类加载器进行加载,因此该阶段是整个加载过程中可控性最强的阶段。在加载阶段,类需要完成以下三个过程:

  • 通过类的全限定名获取类的二进制字节流(除了从class文件获取字节流,还可以通过网络、zip包、动态生成等多种方式获取)
  • 将字节流的静态存储结构转化成方法区的运行时数据结构
  • 在内存中生成这个类的Class对象,作为方法区该类各种数据结构的访问入口

加载与连接阶段部分内容是交叉进行的,加载部分尚未完成,连接阶段可能已经开始。

验证

验证阶段是为了确保字节流中的数据符合虚拟机的要求,验证部分主要分为以下四个阶段:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

如果运行的代码已经反复使用和验证过,可以使用-Xverify:none参数关闭大部分验证措施,缩短类加载时间。

准备

准备阶段是为类变量(static修饰的变量)分配内存并设置初始值的过程。变量所使用的内存将在方法区中分配(实例变量在对象实例化时会随对象一起分配在堆内存中)。初始值一般情况下是指数据的零值,如:

1
public static int i = 123;

变量i的初始值为0而不是123。(特殊情况:若类变量为常量,初始化时会将其赋指定值)

解析

解析过程是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用:以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机内存布局无关,引用目标不一定已经加载到内存中。
  • 直接引用:直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用与虚拟机内存布局相关,引用目标在内存中已经存在。

初始化

初始化阶段是类加载的最后一步,将会根据代码中的赋值动作为变量进行赋值。

类加载器与双亲委派模型

类加载器

要明白双亲委派模型首先需要了解类加载器。所谓类加载器的作用就是通过类的全限定名将类的二进制字节流加载到内存中,并生成class对象。
类加载器一般可以分为启动类加载器,扩展类加载器,应用程序类加载器三种,每个加载器的作用如下:

  • 启动类加载器(Bootstamp ClassLoader):负责加载<JAVA_HOME>/lib或者-Xbootclasspath指定的目录下能被虚拟机识别(按文件名识别,名称不符无法被加载)的类库。该启动器由c++所写,无法被java直接引用,随JVM一起启动。
  • 扩展类加载器(Extension ClassLoader):加载<JAVA_HOME>/lib/ext或者被java.ext.dirs系统变量所指定的目录。
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上所指定的类库。

双亲委派模型

多个加载器之间的层次结构如下图所示,除了启动类加载器之外,其他加载器都有对应的父类加载器:
paste image

定义

当一个类加载器收到加载请求时,首先会将请求交给父类加载器进行加载,按照上图的层次结构一直到达启动类加载器,只有当父类加载器无法加载时,类加载器自己才会完成这个请求。

上述的这个工作过程就称为双亲委派模型。

意义

避免内存中出现多份相同的字节码,比如类A,B两个类都引用System类,因为有双亲委派模型的存在,A,B两个类中的System最终都会交给Bootstamp ClassLoader加载,Bootstamp ClassLoader若发现已经加载过System,则会直接返回内存中的对象,不会多次加载。

自定义ClassLoader

查看ClassLoader源码我们可以发现,虚拟机在加载类时会调用loadClassInternal方法,该方法的唯一作用就是调用loadClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This method is invoked by the virtual machine to load a class.
private Class loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}

在loadClass中首先会判断当前类是否已经加载,已经加载直接返回,没有加载的话则会尝试使用父类加载器进行加载,只有当父类加载器无法完成加载时才会调用本身的findClass方法

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

因此在自定义类加载器时只需要覆写findClass即可,这样可以保证自定义的类加载器是符合双亲委派模型的。

验证双亲委派模型

我们可以自定义一个类加载器进行验证,自定义类加载器代码如下:

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
/**
* 加载指定路径下的class文件
*
* @author zhangwl
* @date 2018/3/29 8:43
*/
public class MyClassLoader extends ClassLoader {
private String path;

public MyClassLoader(ClassLoader parent, String path) {
super(parent);
this.path = path;
}

public MyClassLoader(String path) {
this.path = path;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String newName = name;
//class文件路径
path = path + newName.replaceAll("\\.","//") + ".class";
InputStream is = null;
try {
is = new FileInputStream(path);
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name,bytes,0,bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}

在项目和桌面上各定义一个Person类,包含有say方法。

1
2
3
4
5
6
7
8
9
/**
* @author zhangwl
* @date 2018/3/29 10:03
*/
public class Person {
public void say(){
System.out.println("Hello World Project");
}
}

测试类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author zhangwl
* @date 2018/3/29 9:22
*/
public class Test {
@org.junit.Test
public void MyClassLoaderTest() throws Exception{
MyClassLoader classLoader = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\");
Class cls = classLoader.loadClass("Person");
Method say = cls.getMethod("say");
Object demo = cls.newInstance();
say.invoke(demo);
}
}

可以发现测试类最终结果如下,由于双亲委派模型的存在,自定义的findClass并没有被调用:
mark

我们可以将测试代码稍作修改,显式指定父类加载器为null,

mark

mark

由于无法通过父加载器进行加载,最终调用了我们自定义的findclass方法。

参考

  • https://www.cnblogs.com/lanxuezaipiao/p/4138511.html
  • https://www.cnblogs.com/editice/p/5420712.html
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践》

虚拟机性能监控与故障处理工具

发表于 2018-03-25 | 分类于 JVM高级特性

JDK命令行工具

​ jdk监控和故障处理工具

名称 主要作用
jps 显示指定系统内所有的HotSpot虚拟机进程
jstat 用于收集HotSpot虚拟机各方面的运行数据
jinfo 显示虚拟机配置信息
jmap 生成虚拟机内存转储快照(heapdump)文件
jhat 用于分析heapdump文件,它会建立一个服务器,让用户可以在浏览器上查看分析结果
jstack 显示虚拟机的线程快照

jps:虚拟机进程状况工具

jps命令格式:

1
jps [options][hostid]

其中hostid为RMI注册表中注册的主机名,jps其它常用选项如下:

选项 作用
-q 只输出LVMID,省略主类的名称
-m 输出虚拟机进程启动时传给主类main方法的参数
-l 输出主类的全名,若进程执行的是jar包,输出jar路径
-v 输出虚拟机进程启动时JVM参数

jstat:虚拟机统计信息监视工具

jstat命令格式:

1
jstat [option vmid [interval [s|ms] [count]]]

注:

  • 若为本地虚拟机进程,VMID和LVMID是一致的,若是远程虚拟机进程,VMID格式为:
1
[protocol:][//]lvmid[@hostname[:port]/servername]
  • interval和count表示查询间隔和次数,若不填只查询一次,如 jstat -gc 3243 250 20 表示每个250毫秒查询进程3243的垃圾收集状况,一共查询20次。

option选项以及参数请参考下表:

选项 作用
-class 监视类装载,卸载数量,总空间以及类装载所消耗的时间。
-gc 监视java堆状况,包括堆中各个区域容量,已用空间,GC时间合计等。
-gccapacity 监视内容和-gc基本相同,但主要关注各个区域使用的最大,最小空间。
-gcutil 监视内容和-gc基本相同,但主要关注各个区域已使用空间占总空间的百分比。
-gccause 与-gcutil一样,但会额外输出上一次gc产生的原因。
-gcnew 监视新生代gc状况。
-gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大,最小空间。
-gcold 监视老年代gc状况。
-gcoldcapacity 监视内容与-gcold基本相似,输出主要关注使用到的最大,最小空间。
-gcpermcapacity 输出永久代最大,最小空间。
-compiler 输出JIT编译器编译过的方法,耗时等信息。
-printcompilation 输出已经被JIT编译的方法

jinfo:Java配置信息工具

jinfo命令格式:

1
jinfo [option] pid

如查询CMSInitiatingOccupancyFraction参数值:

1
jinfo -flag CMSInitiatingOccupancyFraction 1444

jmap:Java内存映射工具

jmap命令格式:

1
jmap [option] vmid

option参数如下表:

选项 作用
-dump 生成java堆转储快照,格式:-dump:[live,]format=b, file=,live参数说明是否只dump出存活对象。
-finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象。只在linux/solaris平台下有效。
-heap 显示java堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在Linux/Solaris平台下有效。
-histo 显示堆中对象统计信息,包括类,实例数量、合计容量。
-permstat 以ClassLoader为统计口径显示永久代内存状态。只在linux/solaris平台下有效。
-F 当虚拟机进程堆-dump选项没有响应时,可使用这个选项强制生成dump快照。

jstack:Java堆栈跟踪工具

jstack命令格式:

1
jstack [option] vmid

option选项如下表:

选项 作用
-F 当正常输出的请求不被响应时,强制输出线程堆栈。
-l 除堆栈外,显示关于锁的附加信息。
-m 如果调用本地方法的话,可现实C/C++的堆栈。

JDK可视化工具

​ 除了命令行工具外,还可以使用bin目录下的jconsole或者使用VisualVM等可视化工具对虚拟机进行监视。

垃圾收集器以及内存分配策略

发表于 2018-03-22 | 分类于 JVM高级特性

判断对象的状态

​ 在垃圾回收器对对象进行回收之前首先需要判断对象是否存活,即是否会被引用。若对象不再被任何途经使用,则回收它,否则需要保留该对象。判断对象是否存活主要由以下两种算法。

引用计数法

​ 给对象添加一个引用计数器,对象被引用计数器加一,引用失效减一,若计数器为零则说明对象没有被引用。

​ 该算法实现简单,但主流虚拟机都没有采用该算法,因为该算法无法解决循环引用的问题。即有A,B两个对象,A中某个字段指向B,B中某个字段指向A,两者计数器都不为零,但实际上两者都不会被使用,然而垃圾回收器却无法回收。

可达性分析算法

​ 该算法是目前的主流实现算法,该算法的核心思想是通过一系列的对象作为起始点(称为“GC Roots”),由该节点中所引用的对象开始向下搜索,搜索所走过的路径称为“引用链”,若某对象无法与任何引用链相连,说明该对象不可用,虚拟机将判定其为可回收。

mark

​ 如图所示,obj6,obj7,obj8虽然相互有引用,但他们与GC Roots没有联系,因此他们将会被判定为可回收。

​ 在java中,可作为GC Roots的对象有以下几种:

  • 虚拟机栈(栈帧中的本地变量表)所引用的对象
  • 方法区中静态类型引用的变量
  • 方法区中常量引用的对象
  • 本地方法栈(native方法)引用的对象

垃圾收集算法

标记-清除算法

​ 该算法是最基础的垃圾回收算法,后续算法都是在该基础之上进行改进而来。该算法分为两个阶段:

  • 标记:标记出所有需要回收的对象
  • 回收:将标记好的对象统一进行回收

mark

​ 该算法存在的问题:一是效率问题,标记和清除效率都不高。二是空间问题,会产生大量不连续的内存碎片,碎片过多会导致分配较大对象时无法找到足够的可用内存,进而提前触发垃圾回收动作。

复制算法

​ 复制算法解决了标记清除算法的效率问题,复制算法将内存划分为大小相等的两块,每次只使用其中的一块。当这块内存用完后将所有存活的对象复制到另一块内存中,并清空当前这块内存。复制算法每次只对两块内存中的一块进行回收,也无需考虑内存碎片的问题。但该算法将内存缩小为原来的一半,比较浪费内存。

​ 当前主流虚拟机都以该算法来回收新生代。

mark

标记-整理算法

​ 标记的过程和标记-清除算法一致,但标记完不是直接清除,而是将存活的对象移到一边,直接将另外的内存清除。

mark

分代收集算法

​ 当前商业虚拟机的算法都采用的事分代收集算法,即根据对象生命周期的不同将内存分为几块。java堆内存分为新生代和老年代。新生代存活率低,大量对象死去,因此选用复制算法。老年代存活率高,复制算法需要较多内存,因此选用标记-清除算法或标记-整理算法。

内存分配策略

对象优先分配在eden区

​ 大多数情况下,对象在新生代的eden区中分配。若eden区没有足够的空间,虚拟机将发起一次Minor GC。

大对象直接进入老年代

​ 大对象是指需要大量连续内存空间的对象。经常出现大对象会导致内存中还有空间但提前触发垃圾收集以获取足够的连续空间来安置它。

​ 可以设置虚拟机的-XX:PretenureSizeThreshold值,当对象大小超过设置的值时直接进入老年代,避免在eden区和两个survivor区发生大量内存复制。

长期存活的对象进入老年代

​ 虚拟机为每一个对象设置了一个年龄计数器,当对象在eden区经历第一次Minor GC后仍然存活,则对象会被移到survivor区,并将年龄设为1。在survivor区每经历一次Minor GC年龄加1,当年龄达到一定阈值(默认15)将会被晋升到老年区。阈值可通过 -XX:MaxTenuringThreshold 进行设置。

动态判定对象年龄

​ survivor中对象并非年龄一定要达到MaxTenuringThreshold才会进入老年代。若survivor中相同年龄的对象的大小总和超过survivor空间的一半,则虚拟机会将年龄大于等于该年龄的对象直接移到老年区。

空间分配担保

​ 在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明Minor GC是安全的,否则,虚拟机需要查看HandlePromotionFailure的值,看是否运行担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次Minor GC;若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC,以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
​ 冒险是指经过一次Minor GC后有大量对象存活,而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把survivor区无法容纳的对象直接进入老年代。

mark

mark

垃圾收集器

参考这篇文章《Java虚拟机垃圾回收(三) 7种垃圾收集器 主要特点 应用场景 设置参数 基本运行原理》

参考

  • http://www.cnblogs.com/parryyang/
  • https://www.cnblogs.com/xiaoxi/p/6557473.html
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

JVM内存结构

发表于 2018-03-21 | 分类于 JVM高级特性

​ java虚拟机在运行过程中会将其所管理的内存划分为多个区域,根据各自用途可以大体划分为以下四个区域:堆,栈,方法区,程序计数器,其中栈又可分为本地方法栈和虚拟机栈,下图可以直观的显示内存中各个区域的划分情况。

mark

程序计数器

​ 程序计数器在内存中只占了很小一部分,用来指向下一条指令的地址,字节码解释器在执行过程中根据程序计数器的值选取执行的指令。

​ java通过在不同线程之间切换来执行多线程任务,同一个处理器同一时刻只能处理一个线程,为了保证切换之后能获取之前的执行状态,因此每个线程都拥有一个单独的程序计数器,多个线程之间的计数器互不影响。

​ 若当前线程执行的是java方法,程序计数器的值为所执行指令的地址;若执行的native方法(非java代码),则值为undefined。

​ 此区域是唯一一个不存在OutOfMemoryError的区域。

堆(heap)

​ 堆是虚拟机管理的内存中最大的一块,在虚拟机启动时即被创建,所有线程共享该区域。堆上存放对象的实例和数组。主流虚拟机堆内存的大小都是可调节的(通过-Xms和-Xmx控制),若堆中没有内存可供实例分配,将抛出OutOfMemoryErroe错误。

​ 堆是垃圾管理器管理的主要区域,由于现在的GC基本采用分代收集算法,因此堆还可以分为:新生代和老年代,新生代又可细分为Eden,From Survivor,To Survivor。(见参考文章 1)

方法区

​ 方法区又叫静态区域,与堆一样,所有线程共享该区域。方法区存放被虚拟机加载的类的信息,常量,静态变量,编译后的代码,当方法区无法满足内存分配时,将抛出OutOfMemoryErroe错误。

运行时常量池

​ 运行时常量池是方法区的一部分,受方法区内存限制,内存不足时将会抛出OutOfMemoryErroe错误。要理解运行时常量池需要理解以下三个概念:

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。
    • 字面量:文本字符串、声明为final的常量值等。
    • 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。

mark

java虚拟机栈

​ 和程序计数器一样,虚拟机栈也是线程私有的,生命周期跟线程相同。虚拟机栈表示Java方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操作数栈、动态链接、方法出口等信息(见参考资料4)。每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。

mark

本地方法栈

​ 作用和虚拟机栈类似,虚拟机栈执行java方法,本地方法栈执行native方法(非java代码)。

参考资料

  1. https://www.cnblogs.com/E-star/p/5556188.html
  2. http://blog.csdn.net/bluetjs/article/details/52874852
  3. http://blog.csdn.net/sunshine__me/article/details/49992909
  4. https://www.cnblogs.com/Codenewbie/p/6184898.html
  5. 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》
卖黄瓜的小花菜

卖黄瓜的小花菜

9 日志
5 分类
11 标签
GitHub E-Mail
© 2018 卖黄瓜的小花菜
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4
本站访客数 人次 本站总访问量 次